状态压缩算法的实现

简介: 状态压缩算法的实现

题目描述

n×mn×m的棋盘可以摆放不同的1×21×2小方格的种类数。

题型

状态压缩dp

更新

2022年5月19日16点36分更新

增加适当的空格和换行,愉悦读者体验。

带有图示的题解

Acwing291. 蒙德里安的梦想:状态压缩dp

C++ 代码

/*
下文对 if ((j & k ) == 0 && st[ j | k] ) 有清晰的解释!!!
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 12, M = 1<< N;
long long f[N][M] ;// 第一维表示列, 第二维表示所有可能的状态
bool st[M]; //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。
//vector state[M]; //二维数组记录合法的状态
vector<vector> state(M); //两种写法等价:二维数组

int m, n;

int main() {

while (cin >> n >> m, n || m) { //读入n和m,并且不是两个0即合法输入就继续读入
    //第一部分:预处理1
    //对于每种状态,先预处理每列不能有奇数个连续的0
    for(int i = 0; i < (1 << n); i ++) {
        int cnt = 0 ;//记录连续的0的个数
        bool isValid = true; // 某种状态没有奇数个连续的0则标记为true
        for(int j = 0; j < n; j ++) { //遍历这一列,从上到下
             if ( (i >> j) & 1) {  
                 //i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位; 
                 // &1为判断该位是否为1,如果为1进入if
                if (cnt & 1) { 
                //这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
                    isValid =false; break;
                } 
                cnt = 0; // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。
                //其实清不清零没有影响
             }
             else cnt ++; //否则的话该位还是0,则统计连续0的计数器++。
        }
        if (cnt & 1)  isValid = false; //最下面的那一段判断一下连续的0的个数
        st[i]  = isValid; //状态i是否有奇数个连续的0的情况,输入到数组st中
    }
    //第二部分:预处理2
    // 经过上面每种状态 连续0的判断,已经筛掉一些状态。
    //下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突
    for (int j = 0; j < (1 << n); j ++) { //对于第i列的所有状态
        state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
        for (int k = 0; k < (1 << n); k ++) { //对于第i-1列所有状态
            if ((j & k ) == 0 && st[ j | k]) 
            // 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行) 
            //解释一下st[j | k] 
            //已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
            //我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,
            //还要考虑自己这一列(i-1列)横插到第i列的
            //比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
            //那么合在第i-1列,到底有多少个1呢?
            //自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
            //这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的
                state[j].push_back(k);  
                //二维数组state[j]表示第j行, 
                //j表示 第i列“真正”可行的状态,
                //如果第i-1列的状态k和j不冲突则压入state数组中的第j行。
                //“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
        }
    }
    //第三部分:dp开始
    memset(f, 0, sizeof f);  
    //全部初始化为0,因为是连续读入,这里是一个清空操作。
    //类似上面的state[j].clear()
    f[0][0] = 1 ;// 这里需要回忆状态表示的定义
    //按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
    //首先,这里没有-1列,最少也是0列。
    //其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。
    for (int i = 1; i <= m; i ++) { //遍历每一列:第i列合法范围是(0~m-1列)
        for (int j = 0; j < (1<<n); j ++) {  //遍历当前列(第i列)所有状态j
            for (auto k : state[j])    // 遍历第i-1列的状态k,如果“真正”可行,就转移
                f[i][j] += f[i-1][k];    // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
        }
    }
    //最后答案是什么呢?
    //f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
    //即整个棋盘处理完的方案数
    cout << f[m][0] << endl;
}

}

题目分析

摆放方块的时候,先放横着的,再放竖着的。总方案数等于只放横着的小方块的合法方案数。

如何判断,当前方案数是否合法? 所有剩余位置能否填充满竖着的小方块。可以按列来看,每一列内部所有连续的空着的小方块需要是偶数个。

这是一道动态规划的题目,并且是一道 状态压缩的dp:用一个N位的二进制数,每一位表示一个物品,0/1表示不同的状态。因此可以用0→2N−1(N二进制对应的十进制数)0→2N−1(N二进制对应的十进制数)中的所有数来枚举全部的状态。

状态表示

状态表示:f[i][j]f[i][j] 表示已经将前 i -1 列摆好,且从第i−1i−1列,伸出到第 ii 列的状态是 jj 的所有方案。其中j是一个二进制数,用来表示哪一行的小方块是横着放的,其位数和棋盘的行数一致。

状态转移

状态转移

既然第 i 列固定了,我们需要看 第i-2 列是怎么转移到到第 i-1列的(看最后转移过来的状态)。假设此时对应的状态是k(第i-2列到第i-1列伸出来的二进制数,比如00100),k也是一个二进制数,1表示哪几行小方块是横着伸出来的,0表示哪几行不是横着伸出来的。

它对应的方案数是 f[i−1,k]f[i−1,k] ,即前i-2列都已摆完,且从第i-2列伸到第i-1列的状态为 k 的所有方案数。

这个k需要满足什么条件呢?

首先k不能和 j在同一行(如下图):因为从i-1列到第i列是横着摆放的12的方块,那么i-2列到i-1列就不能是横着摆放的,否则就是1 3的方块了!这与题意矛盾。所以 k和j不能位于同一行。

既然不能同一行伸出来,那么对应的代码为(k & j ) ==0 ,表示两个数相与,如果有1位相同结果就不是0, (k & j ) ==0表示 k和j没有1位相同, 即没有1行有冲突。

既然从第i-1列到第i列横着摆的,和第i-2列到第i-1列横着摆的都确定了,那么第i-1列 空着的格子就确定了,这些空着的格子将来用作竖着放。如果 某一列有这些空着的位置,那么该列所有连续的空着的位置长度必须是偶数

总共m列,我们假设列下标从0开始,即第0列,第1列……,第m-1列。根据状态表示f[i ] [j] 的定义,我们答案是什么呢? 请读者返回定义处思考一下。答案是f[m][0], 意思是 前m-1列全部摆好,且从第m-1列到m列状态是0(意即从第m-1列到第m列没有伸出来的)的所有方案,即整个棋盘全部摆好的方案。

时间复杂度

dp的时间复杂度 =状态表示× 状态转移

状态表示 f[i][j] 第一维i可取11,第二维j(二进制数)可取211211 ,所以状态表示 11×21111×211

状态转移 也是211211

所以总的时间复杂度

11×211×211≈4×10711×211×211≈4×107 可以过

=


相关文章
|
6月前
|
算法 测试技术 C++
【动态规划】【状态压缩】【C++算法】1815 得到新鲜甜甜圈的最多组数
【动态规划】【状态压缩】【C++算法】1815 得到新鲜甜甜圈的最多组数
|
6月前
|
算法 JavaScript Java
【状态压缩】【动态规划】【C++算法】1125.最小的必要团队
【状态压缩】【动态规划】【C++算法】1125.最小的必要团队
|
6月前
|
算法 测试技术 C#
【状态压缩】【动态规划】【C++算法】691贴纸拼词
【状态压缩】【动态规划】【C++算法】691贴纸拼词
|
24天前
|
存储 JSON 算法
TDengine 检测数据最佳压缩算法工具,助你一键找出最优压缩方案
在使用 TDengine 存储时序数据时,压缩数据以节省磁盘空间是至关重要的。TDengine 支持用户根据自身数据特性灵活指定压缩算法,从而实现更高效的存储。然而,如何选择最合适的压缩算法,才能最大限度地降低存储开销?为了解决这一问题,我们特别推出了一个实用工具,帮助用户快速判断并选择最适合其数据特征的压缩算法。
30 0
|
4月前
|
算法 Java
Java面试题:解释垃圾回收中的标记-清除、复制、标记-压缩算法的工作原理
Java面试题:解释垃圾回收中的标记-清除、复制、标记-压缩算法的工作原理
59 1
|
4月前
|
算法 Java 程序员
Java面试题:解释Java的垃圾回收机制,包括常见的垃圾回收算法。介绍一下Java的垃圾回收算法中的标记-压缩算法。
Java面试题:解释Java的垃圾回收机制,包括常见的垃圾回收算法。介绍一下Java的垃圾回收算法中的标记-压缩算法。
50 0
|
6月前
|
存储 编解码 算法
图像的压缩算法--尺寸压缩、格式压缩和品质压缩
图像的压缩算法--尺寸压缩、格式压缩和品质压缩
122 0
|
6月前
|
机器学习/深度学习 人工智能 算法
【图像版权】论文阅读:CRMW 图像隐写术+压缩算法
【图像版权】论文阅读:CRMW 图像隐写术+压缩算法
52 0
|
6月前
|
机器学习/深度学习 存储 编解码
利用深度学习优化视频压缩算法
【4月更文挑战第28天】随着数字媒体时代的到来,视频数据量急剧增加,有效的视频压缩技术变得尤为重要。本文探讨了一种基于深度学习的视频压缩框架,旨在提高压缩效率同时保持较高的视频质量。通过使用卷积神经网络(CNN)对视频帧进行特征提取,并结合先进的编码技术,本研究提出了一种新的率失真优化算法。实验结果表明,该算法在多个标准测试序列上相比传统方法能显著降低比特率,同时维持了良好的视觉质量。
|
6月前
|
算法 测试技术 C++
【状态压缩】【动态规划】【C++算法】691贴纸拼词
【状态压缩】【动态规划】【C++算法】691贴纸拼词
下一篇
无影云桌面