[vjudge HDU - 1400] Mondriaan’s Dream
题目链接
大致题意:
在 n × m 的棋盘内铺满 1 × 2 或 2 × 1 的多米诺骨牌,求方案数。
解题思路:
状压DP
状压的话,骨牌的表示状态不同,对应的解法也不同,这里把两种都讲一下
状态表示:f[i][s1]表示第i-1行已经放满且第i行状态是s1的方案数
s1表示第i行的状态,s2表示第i-1行的状态
(1) 骨牌横放表示00,竖放表示10(上1下0) 下凸形
-
对于竖放的骨牌,相邻两行不能全是1,那么判断条件1:s1&s2=0
-
对于横放的骨牌,0的个数必然是偶数(不包括竖放骨牌下的0),那么判断条件2:s1|s2得到的状态必须是连续偶数个0
(2) 骨牌横放表示11,竖放表示01(上0下1)上凸形
-
第i行的第j列为1,第i-1行的第j列为1(说明第i行的第j列一定不是竖放而是横放,因为横放不能相邻两行全为1),那么判断条件:第i行和第i-1行的第j+1列都要为1
-
第i行第j列为1,第i-1行第j列为0,(说明第i行第j列应该竖放并填充第i-1行第j列)
-
第i行第j列为0,说明不放方块,那么第i-1行第j列必须为1,否则没法填充这个格子。若第i-1行第j列也为0,则不合法 (第i行第j列这个格子空着是留出来给第i+1行竖放的时候插进来)
状压(2)加入了优化,才可以和状压(1)持平,详情见代码注释,这里就不赘述了
轮廓线DP
用上图解释一下轮廓线是个什么东西,红色表示已经放置了骨牌,绿色的格子表示正在进行操作的格子,蓝色的格子就像是一条线,也就是轮廓线,它是不确定的状态,他会根据绿色格子的状态进行改变
状态表示:f[i][j][s]表示已经考虑到第i行第j列,且当前轮廓线上的状态是s的方案数,用滚动数组优化,f[2][1<<m]
我们还是用图片来表示放置骨牌的四种情况
-
0表示空着,1表示被占着
-
对于第一种,只能竖着放,如果横着放,绿色上面的蓝色格子就会一直空着,永远不会被覆盖
-
对于第二种,可以横着放,也可以不放
-
对于第三种,只能竖着放
-
对于第四种,只能空着
if (s >> j & 1) { //被覆盖
f[cur][s ^ 1 << j] += f[cur ^ 1][s]; //不放
}
else { //没有被覆盖
if (j != m - 1 && (!(s >> j + 1 & 1)))f[cur][s ^ 1 << j + 1] += f[cur ^ 1][s]; //横放
f[cur][s ^ 1 << j] += f[cur ^ 1][s]; //竖放
}
- 可以发现不放和竖放可以合并
AC代码:
状压(1)
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 12;
int n, m;
int vis[1 << N];
ll f[N][1 << N];
int main(void)
{
while (cin >> n >> m, n) {
memset(f, 0, sizeof f);
//预处理连续偶数个0的合法状态
for (int i = 0; i < (1 << m); ++i) {
vis[i] = 1;
int cnt = 0;
for (int j = 0; j < m; ++j) {
if (i & (1 << j)) {
if (cnt & 1)vis[i] = 0;
cnt = 0;
}
else cnt++;
}
if (cnt & 1)vis[i] = 0;
}
//初始化
f[0][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 0; j < (1 << m); ++j)
for (int k = 0; k < (1 << m); ++k) {
if ((j & k) == 0 && vis[j | k])
f[i][j] += f[i - 1][k];
}
cout << f[n][0] << endl;
}
return 0;
}
状压2
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 12;
int n, m;
ll f[N][1 << N];
ll res[N][1 << N];
bool check1(int state) { //判断连续偶数个1
for (int i = 0; i < m;)
if (state & (1 << i)) {
if (i == m - 1)return false;
if (state & (1 << (i + 1)))i += 2;
else return false;
}
else i++;
return true;
}
bool check2(int i, int j) {
for (int k = 0; k < m; ) {
if (i & (1 << k)) { //i行k列是1
if (j & (1 << k)) { //i-1行k列是1,那么第i行必然是横放
//第i行和第i - 1行的第j + 1都必须是1
if (k == m - 1 || !(i & (1 << (k + 1))) || !(j & (1 << (k + 1))))return false;
else k += 2;
}
else k++;//第i-1行第j列为0,说明第i行第j列是竖放
}
else { //第i行第j列为0,那么第i-1行的第j列必须是1,否则不合法
if (j & (1 << k))++k;
else return false;
}
}
return true;
}
void solve() {
for (int i = 0; i < (1 << m); ++i)
if (check1(i)) {
f[1][i] = 1; //初始化第i行的合法状态
}
for (int i = 2; i <= n; ++i) //行
for (int j = 0; j < (1 << m); ++j) //第i行状态
for (int k = 0; k < (1 << m); ++k) //第i-1行状态
if (check2(j, k))
f[i][j] += f[i - 1][k];
//优化 记录答案
res[n][m] = res[m][n] = f[n][(1 << m) - 1];
cout << f[n][(1 << m) - 1] << endl;
}
int main(void)
{
memset(res, -1, sizeof res);
while (cin >> n >> m, n) {
memset(f, 0, sizeof f);
//优化 枚举的状态数减少
if (n < m)swap(n, m);
if (!res[n][m]) {
cout << res[n][m] << endl;
continue;
}
//特判奇数行奇数列
if (n & 1 && m & 1) {
puts("0");
res[n][m] = res[m][n] = 0;
continue;
}
solve();
}
return 0;
}
轮廓线DP
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 1; i <= (n); ++i)
using namespace std;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 12;
int n, m;
ll f[2][1 << N];
int main(void)
{
while (cin >> n >> m, n) {
if (n > m)swap(n, m);
memset(f, 0, sizeof f);
int cur = 0;
f[cur][0] = 1;
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j) {
cur ^= 1;
memset(f[cur], 0, sizeof(f[cur]));
for (int s = 0; s < 1 << m; ++s) {
if (f[cur ^ 1][s]) {
//!(s & (1 << j)) && !(s & (1 << j + 1)) 等同于 !(s >> j & 3)
if (j != m - 1 && !(s & (1 << j)) && !(s & (1 << j + 1))) f[cur][s ^ 1 << j + 1] += f[cur ^ 1][s];
f[cur][s ^ 1 << j] += f[cur ^ 1][s];
}
}
}
cout << f[cur][0] << endl;
}
return 0;
}