【无标题】

蒙德里安的梦想

动态规划

(1)化零为整:把不同方案归到一类去,用一个状态来表示

(2)化整为零:把每一个状态再分割成若干个子集,分别去求


题解,视频链接

AcWing 291. 蒙德里安的梦想 - AcWing

9.93 蒙德里安的梦想 状态压缩DP——信息学竞赛培训课程_哔哩哔哩_bilibili

分析

横着的摆完后,竖着的只有一种摆的方案,所以所有方案就是横着摆的所有方案

j用二进制数表示,但是用十进制存储,如果某一行从第i - 1 列伸到第 i 的话就为1,没伸出来就用0表示,如11001则表示1,2,5伸出来

状态计算(集合划分): 第 i - 1 列伸到第 i 列的小方格是固定的,我们在做分割的时候是找最后一个不同点,最后一个没有固定的是从 i - 2 列伸到i - 1列的小方格,状态用 k (k最多可能有2^n种可能)表示, k也是一个二进制数,如第三行从i - 2 列伸到第 i - 1列就是 00100,f[ i , j ]集合中倒数第二步一定属于这某一类(即不同的状态 k)里面,所以这种划分方案是不重不漏的,最后f[ i , j ]里的方案数,就是每个子集里(即不同的状态 k)方案数之和,

状态为k的集合的数量就是f[ i - 1 , k ], 但是我们要求k , 和 j 拼在一起要构成一个合法方案

(1),要求k 和 j ,不能在同一行都有1, 即( j & k )== 0

(2), 所有连续空着的位置的长度必须是偶数

最后把所有合法的k累加到一块,就是我们 f [ i , j ] 的值

最后答案是什么呢?

假设下标从0开始,我们一共有m 列,最后一列是m - 1 列, 则我们的答案是 f [ m , 0 ]; (这里的下标m是第m + 1列,因为下标是从0 开始),它指的是前 m - 1列(即下标是m - 1,也就是第m列)已经摆好(即棋盘已经摆好),且从m - 1列伸到第m列的所有方案的状态是0,也就是没有任何一个方块伸出来的所有方案,恰好是摆满n * m 这个棋盘的所有方案

时间复杂度分析

二维, 第一维是11, 第二维是2^11,状态也会有2^11种,则最坏时间复杂度就是11 * 2^11 * 2^11

优化

预处理:对于每个状态k 而言,有哪些状态可以更新到j,先把所有合法方案预处理出来,

朴素写法,1000ms

#include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 12, M = 1 << N; int n, m; long long f[N][M]; bool st[M]; int main() { while (cin >> n >> m, n || m) { for (int i = 0; i < 1 << n; i ++ ) { int cnt = 0; st[i] = true; for (int j = 0; j < n; j ++ ) if (i >> j & 1) { if (cnt & 1) st[i] = false; cnt = 0; } else cnt ++ ; if (cnt & 1) st[i] = false; } memset(f, 0, sizeof f); f[0][0] = 1; for (int i = 1; i <= m; i ++ ) for (int j = 0; j < 1 << n; j ++ ) for (int k = 0; k < 1 << n; k ++ ) if ((j & k) == 0 && st[j | k]) f[i][j] += f[i - 1][k]; cout << f[m][0] << endl; } return 0; }

去除无效状态的优化写法,230ms

#include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std; const int N = 12, M = 1 << N; //M最多2^N,因为会处理到M + 1列,所以棋盘要多开一个 //左移k相当于乘2^k typedef long long LL; int n, m; LL f[N][M]; //状态转移方程 vector<int> state[M]; //vector存的是所有合法状态,对所有状态而言,所有能转移到它的合法状态有哪些 bool st[M]; //st存的是某个状态是否合法,判断当前这一列能否用1*2的小方格填满,即当前这一列所有空着的连续位置是否是偶数 ///存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。 int main () { while (cin >> n >> m, n || m) //输入n和m,直到n和m同时为0 { for(int i = 0; i < 1 << n; i ++) //预处理:判断合并列的状态i是否合法,即连续的0是否是偶数个 { //预处理st数组存所有状态里所有 连续的0是否是偶数个 //i从0到2的输入的n的次方-1,遍历二进制的每一位数,一共有2^n种状态 //每一位取0,二级制为0,十进制为0,每一位为1,二进制为11……11,十进制为2^n - 1 //所以从0取到2^n -1 int cnt = 0; //cnt表示当前连续0的个数 bool is_valid = true; //bool未赋初值是false //is_valid表示是否合法,一开始是true // 某种状态没有奇数个连续的0则标记为true for(int j = 0; j < n; j ++) //j是表示行数,为了取出i中的每一位数,所以从0开始,从1开始的话就取不出i的最低位了 //如 2 >> 1 ,2的二进制为10, 右移1为1,取不出最低位的0 //横着做,每行都会有n个数 //遍历这一列,从上到下 if(i >> j & 1) //如果当前这一位是1的话 { //i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位; // &1为判断该位是否为1,如果为1进入if,是0,则eles cnt ++ if(cnt & 1) //判断(****这里的前面是指的低位上的****)前面的0是否有奇数个,为奇数则执行下面,偶数不执行 //这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法 //奇数则cnt 的二进制的第0位是1,与上1则为1,偶数则第0位为0,与上1则为0 { is_valid = false; //判断前面的0是否有奇数个,如果前面0奇数个的话,说明st为不合法,先用is_valid存false break; //不合法直接break } cnt = 0; //把cnt清空 // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。 //cnt 为偶数,即偶数个0, 其实清不清零没有影响 } else cnt ++; //否则这一位是0, 则统计连续0的计数器++。 if(cnt & 1) is_valid = false;//判断一下最后一段0, 最后一段0是奇数也是不合法的 //就是高位上的0,如 0100,执行else cnt ++ 两次为2,执行if(i >> j & 1) ,cnt为偶数,不执行if(cnt & 1),cnt置为0,j等于3,执行else,cnt+为1, //退出 for(int j = 0; j < n; j ++) 时 is_valid依旧是初始的true,而这是不合法的, //再判断一下cnt是不是奇数即可 //如果前面的0是奇数的话,则break了,则这里重复判断了cnt是否为奇数,即前面的0(****这里的前面是指的低位上的****)的个数是否为奇数 //但是影响不大 st[i] = is_valid; //把is_valid存的false或true赋给st //状态i是否有奇数个连续的0的情况,输入到数组st中 } for(int i = 0; i < 1 << n; i ++) //枚举对于第i列的所有状态,循环中的i表示状态,而不是列数 { state[i].clear(); //清空上次操作遗留的状态,防止影响本次状态,本题多组测试数据,不同n合法状态不同 for(int j = 0; j < 1 << n; j ++) //枚举对于第i-1列所有状态 if((i & j ) == 0 && st[i | j]) //合法状态需要满足两条,(1)(2) //先看下所有塞满位置的状态,st[i | j],它应该是合法的 // 第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[i].push_back(j); //把j存到i里去 } memset(f, 0, sizeof f); //所有状态清空一下 f[0][0] = 1; //因为不存在-1列,所以也不会有小方格从-1列伸到第0列的, //j为0表示没有所有从-1列伸到0列的小方块,这种是合法的,即放着的都为竖着的小方块,方案为1种 //j 为其他 都表示有小方格从-1列伸到0列,是不合法的,所以都为0 //f[0][0] = 1表示第-1列已经摆好,且伸到第0列的状态是0() for(int i = 1; i <= m; i ++) //枚举列 for(int j = 0; j < 1 << n; j ++) //j表示当前列的所有状态 for(auto k : state[j]) // 遍历第i-1列的合法状态k f[i][j] += f[i - 1][k]; //f[i][j] 等于i - 1列已经摆好且从i-1列伸到第i列的状态为j(j是第i列的状态)的方案数的总和,当前这列合法状态(合法状态可能有很多种,前面的i-1列也可能有很多种摆法), //f[i][j]就等于∑f[i - 1][k],即前i - 2列已经摆好伸到第i-1列的状态为k的方案数的总和(k就是state[j],**注意**这里的k是,已经预处理过了,就是合法方案) //这里的k表示是i - 1列的合法状态,因为 cout << f[m][0] << endl; } return 0; }

预处理 state[i].push_back(j); 的解释:

用i 和 j 表示任意两列(注意!!是任意两列)的状态(因为任意两列状态合法是可以求出来的)(这里的i与括号外的i不同,这里是棋盘中的列数,括号外是列的状态),在预处理中用一维数组表示两个状态对应合法,还没有给这两个状态定义是哪两列的,(我们可以把i作为i列的状态,把j作为 i - 2的状态,j就是在i- 2列中对应 在i列中的状态为 i 的合法状态,但这是毫无意义的),在后边遍历的时候,j的状态定下来后,则与j对应的合法状态k也就定下来了, 所以可以用二维中的第一维定义 j 是 i - 1 的状态, i 是第 i 列的状态,储存在state[ i ]中的值表示与状态i 对应的合法的状态是 j, 在后面dp状态转移的时候,f[i][j] += f[i - 1][k]; k就表示与状态j相对应的合法的状态,

这样做的目的是找出与一个状态对应的合法状态是什么,在后面状态转移时就不需要每次都枚举一遍,就起到了优化,

因为i 和 j 肯定 不能相等,所以他们可以从0枚举到2^n种状态

i 表示第i 列的状态,二进制表示,十进制储存,j表示第i - 1列的状态

state[ i ]相当一个二维数组,用来存储棋盘中第 i 列 能有哪些合法状态,最多有2^n种,这里的i也相当于二维数组中的第i行,这里行的每一个数组元素都存的是棋盘中这一列的合法状态

最短Hamilton路径

#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 20, M = 1 << N; int n; int w[N][N]; //w是两点之间的距离 int f[M][N]; //f存的是状态 int main () { cin >> n; for(int i = 0; i < n; i ++) for(int j = 0; j < n; j ++) cin >> w[i][j]; memset(f, 0x3f, sizeof f); //其余点初始化为正无穷 f[1][0] = 0;//初始是在0号点,从0走到0,走过的点只有0这一个点,第0位上是1,其余所有位上都是0 for(int i = 0; i < 1 << n; i ++) for(int j = 0; j < n; j ++) if(i >> j && 1) //如果从0走到j的话,i里一定包含j for(int k = 0; k < n; k ++) //枚举它从哪个点转移过来 if(i - (1 << j) >> k & 1) //如果想从k点转移过来,i除去j这个点之后一定包含k这个点,第k位一定得是1才能走到第k位 f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]); cout << f[(1 << n) - 1][n - 1] << endl; //最终答案是(n个1,每一位上都是1)落脚到n - 1这一个点上去 return 0; }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值