题意:用1*2的大小砖块铺设n*m的地面,求铺设方案总数。
类型:铺砖问题&状态DP
分析:关于该铺砖问题,小艾在这里提供两种DP方法。
第一种:用1*2的砖块进行铺设时,砖块可以选择横着放和竖着放两种。对于当前的位置(i, j),若横着放,则使(i, j)和(i, j+1)都置为1;若竖着放,则使(i, j)为0,(i, j+1)为1。那么对于第n行而言则必有(n, j)为1且 1<=j <= m。根据上述分析,我们可以定义一个二维的dp数组,d[i][j] := 第i行状态为j时的铺设方案总数,关于该dp数组的初始化只需枚举第一行的状态即dp[1][j],若通过检查状态j可以存在则使dp[1][j] = 1,关于状态转移方程的思考则是检查第i行的状态j能否向第i+1行的状态k转移,也就是所谓的兼容性,若二者兼容则有dp[i+1][k] += dp[i][j]。此方法的时间复杂度为O(nm2^(2m))。
第二种:我们按照选择从上到下,从左到右的顺序检查并进行砖块的铺设。对于已经铺设到的位置记为true,没有铺设到的位置记为false。那么对于当前检查到的位置(i, j),必有(i, j)之前的位置皆为true。而由于砖块的铺设可以选择横放和竖放两种,只有(i, j)到(i, m),(i+1, 0)到(i+1, j - 1)的状态是不确定的,即(i+1, j)和其后的位置一定不会有砖块放置,故皆为false。综上,我们只需枚举每一列里还没查询到的最上面的位置的状态即可,总共有m个。同样地,定义一个三维dp数组,dp[i][j][k] := 铺设位置(i, j)时状态为k时的铺设方案总数。下面我们来推导该dp的状态转移方程。对于当前的位置(i, j),设枚举的状态为used,检查位置(i, j)是否为需要放置砖块只需看used >> j & 1是否为1,若为1则不必再放置且必有(i+1, j)为0,dp[i][j][used] = dp[i][j+1][used & ~(1 << j)],否则可以在位置(i, j)上选择横放和竖放。可以横放的条件为used>>(j+1)&1 == 0并且j <= m-1,此时的铺设方案数dp[i][j+1][used | (1 << (j+1))];竖放的条件是i+1 <= n,此时的铺设方案数为dp[i][j+1][used | (1 << j)]。如果横放和竖放都可以,则有dp[i][j][used]= dp[i][j+1][used | (1 << (j+1))] + dp[i][j+1][used | (1 << j)]。那么关于该dp数组的初始化又该如何呢?dp[n][m][0] = 1:在铺设到最后一个位置时,便于理解我们不妨在增加一列但该列的状态必须皆为false,所以第n+1列的前m-1个位置加上第n列的最后一个位置的状态只能是used=0且只能选择横放和竖放中的一种。最后只需答案输出dp[1][1][0]:在铺设第一个位置(1, 1)时其余的位置皆为false,used只能为0。此方法的时间复杂度O(nm2^m)。
总结:上述两种方法都可以通过对两个一维数组的滚动循环利用进行空间复杂度的优化,而就时间复杂度上讲第二种方法效率高于第一种。下面的参考代码是关于第二种方法的,第一种方法大家可以尝试写一下。
//实现代码
#include <iostream>
#include <algorithm>
using namespace std;
long long dp[2][1 << (15)];//1 <= n,m <= 15
void solve(int n, int m)
{
int *cur = dp[0], *nxt = dp[1];//滚动数组的利用,dp[s]表状态为s时的方案数
int i, j, k, s = 1 << m;
cur[0] = 1;
for(i = n - 1; i >= 0; --i)
{
for(j = m - 1; j >= 0; --j)
{
for(k = 0; k < s; ++k)
{
if(k >> j & 1)
nxt[k] = cur[k & ~(1 << j)];
else
{
int ans = 0;
if(j + 1 < m && !(k >> (j + 1) & 1))
ans += cur[k | (1 << (j + 1))];
if(i + 1 < n)
ans += cur[k | (1 << j)];
nxt[k] = ans;
}
}
swap(cur, nxt);
}
}
cout << cur[0] << endl;
}
int main()
{
int n, m;
cin >> n >> m;
solve(n, m);
return 0;
}