https://acm.sjtu.edu.cn/OnlineJudge/problem/1092
我们先来看一看这个问题的简化版本:只用 1×2 1 × 2 和 2×1 2 × 1 两种方块覆盖 m×n m × n 的平面。
首先,状态压缩是毋庸置疑的。若某个方块被覆盖则为1,没有被覆盖则为0。这样,每一行的状态可以用一个二进制数来表示,且其转化为十进制的大小不超过 29=512 2 9 = 512 。
为了下文扩展到当前问题,这里用递归来描述解法。由于每个方块最多影响两行,递归时需要的行参数只有当前行和上一行。但是,递归是对列从左往右进行的。具体思路我们在代码中说明。
//设m是总行数,n是总列数
void dp(int row, int col, int now, int last){
//row表示当前是第几行,col表示当前是第几列,now和last分别表示当前行和上一行的状态。
if (col > n)
return;
if (col == n){
f[row][now] += f[row - 1][last];
//f[i][j]表示前i-1行已经完全覆盖,第i行覆盖的状态为j(用二进制状态压缩)的方法数。
}
dp(row, col + 1, (now << 1) | 1, last << 1); //放2*1的方块
/*以这个状态转移为例解释一下参数变化的含义。当前行不变,还是row;2*1的方块只占一列,因此col+1;放过之后,now的这一列是被覆盖的,因此把now右端添加1;能放2*1的方块要求上一行的这个位置本来没有被覆盖,因此把last右端添加0。*/
dp(row, col + 2, (now << 2) | 3, (last << 2) | 3); //放1*2的方块
/*如果放1*2的方块但上一行对应的位置没有被覆盖的话,以后就没有机会再去覆盖了,不符合题意。因此last右端添加两个1。*/
dp(row, col + 1, now << 1, (last << 1) | 1); //不放。同上,上一行的这个位置必须已被覆盖。
}
边界条件:f[0][i] = 0
, f[0][(1 << n) - 1] = 1
。表示第0行事先视为全部铺满,若搜索第一行时出现2*1的方块越界的情况,last(代表第0行)中会出现0,最后进行累加时必然是加0。最后答案是f[m][(1 << n) - 1]
。
以上做法的关键在于理解:我们是在铺当前行,上一行要正好留出空缺。
有了上面的基础,我们再来考虑如何处理要求的变化:缺一角的 2×2 2 × 2 方块。经过多次尝试,我发现不能再要求当前行和上一行始终处于同一列;但是,列数差也不能超过 2 2 。因此,我多加两个只能取 0 0 和 1 1 递归参数exNow
和exLast
。exNow=1
表示当前行比上一行多一列,exLast=0
表示上一行比当前行多一列。递归的思路没有什么变化,不过情况变得很复杂,还要小心重复。具体分类在代码中说明。
void dp(int row, int col, int now, int last, int exNow, int exLast){
if (col > n || (col == n && (exNow || exLast))) //超出边界
return;
if (col == n && exNow == 0 && exLast == 0){ //完全合适
f[row][now] += f[row - 1][last];
return;
}
if (exNow == 0 && exLast == 0){ //正常情况
dp(lim, col + 1, (now << 1) | 1, last << 1, 0, 0); //2 * 1
dp(lim, col + 2, (now << 2) | 3, (last << 2) | 3, 0, 0); //1 * 2, exNow = 0
dp(lim, col + 1, (now << 2) | 3, (last << 1) | 1, 1, 0); //1 * 2, exNow = 1
dp(lim, col + 1, (now << 1) | 1, last << 2, 0, 1); //右下缺口, exLast = 1
dp(lim, col + 2, (now << 2) | 2, last << 2, 0, 0); //右下缺口, exLast = 0
dp(lim, col + 2, (now << 2) | 1, last << 2, 0, 0); //左下缺口
dp(lim, col + 1, (now << 2) | 3, last << 1, 1, 0); //右上缺口, exNow = 1
dp(lim, col + 2, (now << 2) | 3, (last << 2) | 1, 0, 0); //右上缺口, exNow = 0
dp(lim, col + 2, (now << 2) | 3, (last << 2) | 2, 0, 0); //左上缺口
dp(lim, col + 1, now << 1, (last << 1) | 1, 0, 0); //不放, exNow = 0
}
else if (exNow == 1 && exLast == 0){ //当前行超出一列
dp(lim, col + 2, (now << 1) | 1, last << 2, 0, 0); //左下缺口
}
else{
dp(lim, col + 1, (now << 2) | 3, last, 1, 0); //1 * 2, exNow = 1
dp(lim, col + 2, (now << 2) | 3, (last << 1) | 1, 0, 0); //1 * 2, exNow = 0
dp(lim, col + 2, (now << 2) | 3, last << 1, 0, 0); //左上缺口
}
}
读者可以思考一下,为什么没有这几种情况:
1、不放,exNow = 1
或exLast = 1
;
2、exNow = 1
时,缺口不放, col + 1
, exNow = 0
;
3、exLast = 1
时,缺口视为已放, col + 1
, exLast = 0
。
当然,如果把以上情况添加进去,就必须把另一些情况去除来避免重复。
这里把小数据的答案给出,供调试用。
m | n | ans |
---|---|---|
2 | 3 | 5 |
2 | 4 | 11 |
2 | 5 | 24 |
2 | 6 | 53 |
3 | 3 | 8 |
3 | 4 | 55 |
3 | 5 | 140 |