DP專題8 | 骨牌擺放問題 POJ 2411(狀態壓縮DP)

題目:

給你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專題7 | 沒有上司的舞會 洛谷1352(樹形DP)

DP專題 6 | 石子合併 CH5301(區間DP)

DP專題 5 | 顏色的長度 - UVA1625(線性DP)

DP專題 4 | 骨頭收集愛好者 - POJ 1458( 0-1揹包)

DP專題 3 | LCS最長公共子序列 - POJ 1458

DP專題 2 | LIS POJ - 2533(經典DP)

DP專題 1 | 數字三角形 - POJ 1163(簡單DP)

前綴和、二維前綴和與差分的小總結

 

個人公衆號(acm-clan):ACM算法日常

專注於基礎算法的研究工作,深入解析ACM算法題,五分鐘閱讀,輕鬆理解每一行源代碼。內容涉及算法、C/C++、機器學習等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章