题目描述
$n\times m$的棋盘可以摆放不同的$1 \times 2$小方格的种类数。
题型
状态压缩dp
更新
2022年5月19日16点36分更新
增加适当的空格和换行,愉悦读者体验。
带有图示的题解
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<int > state[M]; //二维数组记录合法的状态
vector<vector<int>> 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\rightarrow 2^N-1(N二进制对应的十进制数)$中的所有数来枚举全部的状态。
状态表示
状态表示:$f[i][j]$ 表示已经将前 i -1 列摆好,且从第$i-1$列,伸出到第 $i$ 列的状态是 $j$ 的所有方案。其中j是一个二进制数,用来表示哪一行的小方块是横着放的,其位数和棋盘的行数一致。
状态转移
状态转移
既然第 i 列固定了,我们需要看 第i-2 列是怎么转移到到第 i-1列的(看最后转移过来的状态)。假设此时对应的状态是k(第i-2列到第i-1列伸出来的二进制数,比如00100),k也是一个二进制数,1表示哪几行小方块是横着伸出来的,0表示哪几行不是横着伸出来的。
它对应的方案数是 $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(二进制数)可取$2^{11}$ ,所以状态表示 $11\times2^{11}$
状态转移 也是$2^{11}$
所以总的时间复杂度
$11\times 2^{11} \times 2^{11} ≈ 4 \times 10^7$ 可以过
对于
if(st[k|j]&&(j&k)==0)
更易懂的解释6666
送我一个锐克
好
已保研东南部某985,欢迎咨询
%%%
为什么f[0][0] = 1,说一下我的理解。
首先,题目中输入参数的列数是从1开始到m,即范围为1~m,但我们写的时候是将其先映射到数组0~m-1里;
对于第一列,也就是数组中的第0列,是需要初始化的;也就是我们需要初始化f[0][x] = ?
回到定义,f[0][x] 表示从-1列伸到0列(此处说的都是数组下标)状态为x的方案。我们发现,合法的方案只能是不伸过来,因为根本没有-1列。即x只能取0的时候方案合法,f[0][0] = 1;
接着dp过程就从第1列(数组下标)开始。
那么答案为什么是f[m][0] 呢,因为横放的时候方块最多够到第m-1列(数组下标),不能从m-1再往外伸,
所以是f[m][0];
确实是很好的解释
我觉得,f[m][n]中n理解为从m列伸出去的状态更合理。f[m][0]时,表示当摆到m列满列并没有伸出去的状态时,就是最多方案数了。而f[0][0]可以理解为0列没有延伸出去,因为整个方案都是从第1列开始摆的,那第0列就是为了dp递推而虚构的,当然就不能有延伸而影响到第1列的方案了。
很棒的理解
可以想的简单点,我们定义了f[i][j],其实j就是表示了i-1列开始哪些行放置了1x2的小块,然后合理方案有多少。而f[0][0]就是啥也不放的合理方案是多少,也没必要多想f[-1][x]的情况,初始化就是为了让dp过程自洽,仅此而已,跳出来看,其实还是很容易接受这个初始化的。
我看了一下一种解释,应该更加合理,f[i][j],表示的是第i列横放的情况。这样就说的通了,f[0][0]表示第0列什么也不放,对于答案f[m][0],也就是说第m列不横放,这也是合理的,如果第m列横放了,那就超出m列的范围了。 综上,对于f[i][j]的定义, 表示第i列横放情况为j的方案数,这种定义才是更加自洽的
不错不错
但是如果列数为奇数的话,f[0][0]=1不就解释不通了嘛,毕竟竖着放不开
这个解释优于下面的解释 强!
此题最难理解的是f[0][0] = 1,f[0][0]第一个0表示第0列,第二个0表示第0列伸出去的状态为0,那么第0列的方案只能是竖摆这一种,至于不需要考虑竖摆的奇偶数合法性,因为第0列本身就是为了推导第1列而虚构的。如果假设f[0][0]=0,那么后面所有的递推就无从谈起。一开始很难理解f[0][0]是1还是0,可以先都放到程序里,看下结果是否和手算的一致。
tql,完美解答疑问
不是不摆第0列吗
实际就是从第1列开始摆的,但是第1列合理摆放的前提条件是前列没有砖块插入而改变第1列的奇偶性,所以需要设想有第0列没有砖块插入到第1列,用状态表达数组就是f[0][0]=1。这个状态表达数组的具体含义我在前面详细解释了。
如果n是奇数,比如是3(表示只有3行),那么f[0][0](行数是奇数,竖摆的话,无解)是不是应该等于0?(表示方案数为0)
从逻辑上来说,假设f[0][0]有解,然后在状态转移中推导出有解或无解;如果假设f[0][0]无解,那么状态转移只能是无解,因为前提条件已经不成立了。这个就是逻辑上的先验假设。假设的目的都是为了保证逻辑上的自洽和结果的正确。
感觉和计数dp那个有点像
我觉得需要注意一点的是f[0][0]初始化为1并不意味着只初始化了这一个,剩余f[0][i],i>0有全局数组均初始化为0,因为从-1列到0列本身没有伸出,所以除了0状态其他的方案个数均为0,只有这样才能根据状态方程递推,dp问题都是有初始化的
头像越粉,写题越狠
哈哈哈哈哈
有道理
有道理
有道理
我是这么理解的
把f[i][j]理解为已经摆完前i列并且第i列的状态为j的所有方案。对于j这个二进制数中的1是指我在这个小方格”新”放置了一个横条。也就是这个小方格是一个横条的起始位置。这样代码跟现在的代码是一样的,而且感觉初始化f[0][0]=1也好理解一些。因为我们正常摆第一列的时候是随便自由摆放的,所以f[0][i]中除了f[0][0]其他的都应该是非法的,赋值为0(因为如果i不是0,就代表有些小方格会放置横条,就会影响到第一列的摆放),而对于f[0][0]其含义是已经摆完了前0列并且第0列的状态是0,也就是这一列没有任何一个小方格放置了横条。其摆法只有一种所以赋值为1。而对于f[m][0]也就是已经摆完第m列,并且第m列的状态为0,也就是第m列没有任何一个小方格新去摆放横条,这正好就是我们想要的结果。
这个好哎
# 👍
牛啊牛啊
state[j].push_back(k); 的理解:
假如当前处于某一列, 当前列的可能(即二进制表示的状态)和上一列的可能,对应之后是可行的,就把当前这种可能的状态作为key,将它对应的所有可行的状态存为value
棒棒
orz 真的 你这个题解写的 绝了 太绝了!!!
前人栽树,后人乘凉
1<<n是啥意思
同问
意思是2的n次方
噢噢
我有点不是很理解这个预处理
其实预处理就是找出所有状态是否满足间隔不为奇数,然后一列列遍历
对于if(st[k|j]&&(j&k)==0)的解释:
j&k ==0的意思是你的这一列和上一列不能有重叠的1,两个1位置相同的话就得1说明重叠了是不行的
st[k|j] 的指你的两列合并成一列不能有
奇数0不然就会有空缺,那么这个方案就是不行的,0指的是竖着放只能放00才能放得下
而f[m][0]的意思不是说最后一行全部都放竖着的,而是最后一行只有竖着放的所有的情况和前一列兼容的情况的总和
state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
为啥要加这行代码呀,前面创建完状态数组后也没往里面添加元素呀,应该不用清除吧。可是去掉了就错了
有多组测试数据,下一次的会受到上一次的影响
太厉害了!!!
我的天orz....理解了,但是自己想破脑子也写不出来,只是顺着思路理解了而已
这个讲的特清晰的https://www.cnblogs.com/2huk/p/17297365.html
y总讲的没看懂,看了这篇醍醐灌顶
这篇题解讲的很清楚,有图就更清楚啦,不过自己画记忆更深刻,hh
为什么f[0][0]为1,,我是这样想的,既然没有从不存在的-1列伸出,那么只考虑竖着放在第一列,,那么不是应该行数为奇数时为0,偶数时为0嘛???求解答
f[0][0]=1
是一个初始化,只有第0列的j
为00…0(n个0)时,即第0列没有前一列延伸过来的方块时,才有一个方案,至于你说的奇数偶数,是下一列综合第0列才考虑的所以和梦想有什么关系?
做梦都会想到自己做这道题时候的痛苦
hhhhh
哈哈哈哈哈哈哈