題目:
給你n*m(1<=n,m<=11)的方格矩陣,要求用1*2的多米諾骨牌去填充,問有多少種填充方法。
比如下圖是對於如下2x6的方格矩陣,可能的填充方案之一。
該如何使用動態規劃的方式解決這道題呢?先了解一下狀態壓縮算法。
狀態壓縮通常是使用一個整數來表示一個集合,比如整數3,二進制表示爲11,第一位狀態爲1,第二位狀態爲1,數字2的二進制表示爲10,第一位的狀態爲1,第二位的狀態爲0,數字1的二進制表示爲01,第一位爲0,第二位爲1,數字0的二進制可以表示爲00,兩位的狀態都是0。
這就是說,狀態可以保存在一個整數裏面,對於狀態壓縮DP,其實也是用狀態壓縮後的整數表示一個維度,然後進行狀態轉移。
狀態壓縮DP:採用狀態壓縮算法的DP問題,也就是用整數表示集合,然後將該整數作爲DP的一個維度來進行DP狀態轉移。
希望理解更深的童鞋能夠踊躍發言指正~
繼續看題,其實這道題的解法並不簡單,甚至於有些晦澀,光是理解狀態壓縮DP並不一定能夠看懂源代碼。
首先定義狀態轉移方程:
f(i, j) = f(i-1, k)之和,j, k屬於[0,1<<m)
且滿足 j&k==0
滿足 j|k是有連續個0的狀態
f(i,j)表示第i行的狀態爲j時,前i行的方案總數。且定義狀態1表示豎着的上面一部分,其他狀態爲0。
對於約束的理解是本題的關鍵,
約束1:j&k == 0 是因爲不可能上下都是1。
約束2:j|k合併後狀態0必須是連續偶數個,這個可以預先緩存起來,不用每次都算。詳見代碼註釋。
源代碼:
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <bitset>
#include <iostream>
#include <set>
#include <string>
#include <vector>
using namespace std;
long long f[12][1 << 11];
int in_s[1 << 11];
// 注意這裏一定要用64位的整數
long long solve(int n, int m) {
for (int i = 0; i < 1 << m; ++i) {
int cnt = 0, has_odd = 0;
// 遍歷m中的每一位
for (int j = 0; j < m; ++j) {
if (i >> j & 1) {
//把cnt的值先存放到has_odd上,然後清零
has_odd |= cnt, cnt = 0;
} else {
//如果是偶數個0,則肯定cnt最後爲0,因爲0^1=1 1^1=0
cnt ^= 1;
}
}
// 如果cnt=1則表明存在連續的奇數個0位
in_s[i] = has_odd | cnt ? 0 : 1;
}
f[0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < 1 << m; ++j) {
f[i][j] = 0;
for (int k = 0; k < 1 << m; ++k) {
// 上面是1下面必須是0
// 0必須是連續偶數個
if ((j & k) == 0 && in_s[j | k]) {
// 滿足要求後加上i-1行k的所有情況
f[i][j] += f[i - 1][k];
}
}
}
}
// 最後一行的j所有位都是0
return f[n][0];
}
int main() {
#ifdef __MSYS__
freopen("test.txt", "r", stdin);
#endif
int n, m;
while (cin >> n >> m && n) {
cout << solve(n, m) << endl;
}
return 0;
}
下期預告:DP專題終 - 概率DP
往期回顧
DP專題 5 | 顏色的長度 - UVA1625(線性DP)
DP專題 4 | 骨頭收集愛好者 - POJ 1458( 0-1揹包)
DP專題 3 | LCS最長公共子序列 - POJ 1458
DP專題 1 | 數字三角形 - POJ 1163(簡單DP)
個人公衆號(acm-clan):ACM算法日常
專注於基礎算法的研究工作,深入解析ACM算法題,五分鐘閱讀,輕鬆理解每一行源代碼。內容涉及算法、C/C++、機器學習等。