狀態壓縮動態規划學習筆記
算法介紹
狀態壓縮動態規划是近些年來NOIP提高組常考的算法,也是日后ACM必備的算法之一,因此我們有必須要學習此類高級算法.而且此類算法往往是NP算法的最強優化之一.
算法思想
狀態壓縮動態規划,顧名思義也就是,將動態規划中的狀態數組進行了壓縮.
那么想到壓縮,我們不免就要想到一種常用的時間空間優化技巧,或者說一種特殊的算法,也就是位運算.
卡常算法就是它,高端暴力就是它,奇跡算法還是它.
位運算,也就是二進制的運算,而且我們的二進制,是一種計算機中最為核心的編碼,這也就是為什么,電腦對於這種編碼運算速度最快.
狀態壓縮動態規划,就是利用了位運算,的這三大優化性質,來起到簡化代碼,優化代碼,解決難題的目的.
位運算基礎
| & | | | ^ | << | >> | |
|---|---|---|---|---|---|
| 中文意思 | 並 | 或 | 異或 | 右移 | 左移 |
| 舉例說明 | 1&1=1 | 1|0=1 | 1^0=1 | 1<<1=10 | 10>>1=1 |
| 1&0=0 | 0|0=0 | 1^1=0 | 11<<1=110 | 101>>1=10 |

判斷算法
首先拿到一道題目,我們第一步就是要看數據范圍,題目描述. 而且當你發現數據范圍和題目描述具有以下三大特點的時候,那么我們就可以初步判斷這道題目需要使用狀態壓縮動態規划.
- 數據中的N,M范圍很小,基本上不超過30,20.(N,M廣義理解)
- 題目似乎要我們求方案數,或者說極值問題.
- 題目似乎是個棋盤覆蓋這種類型的問題.
算法處理
一般來說,狀態壓縮動態規划算法,最為困難,也是最為關鍵的一步,就在於
那么接下來我就來詳細解說,狀態壓縮的狀態到底如何設置.
一般來說狀態設置,往往是一個整數,表示一個二進制決策集合.
比如說13,它就可以表示為1011,那么我們一般來說可以表示第一個點,第二點,第四個點已經選擇這個意思.
因為我們可以確定算法為狀態壓縮,那么我們現在的主力攻擊,就是狀態設置,既然現在我們已經有了這個目標,顯然我們就是盡量地將題目的條件進行轉化,在這里我們具體以棋盤類型來分析.
對於條件而言的話,我們需要捕捉到關鍵點.
- 如果說題目中出現了這一個點不可以選擇,那么你的神經中樞第一時間就要條件反射地,對自己的內心說一句,這里是1.
這里是1到底是什么意思?其實這個意思,就是告訴我們這個點不可以選擇,我們可以通過開一個特殊數組來保存,那么到了以后,對於我們枚舉的一個決策集合,那么我們可以通過&運算,來判斷這個點是否可以選擇.
比如說我們現在要求第五個點不可以選擇,那么我們可以構造一個判斷數組.10000表示第五個點不可以選擇.
那么假如說我們當前枚舉的狀態決策集合是11000,這個意思是,我們當前選擇第四個點和第五個點.
那么我們可以通過&運算,來進行判斷.
11000的十進制表示為24,而我們10000表示為16.那么我們進行&運算.
24&16 ==> 11000 & 10000 ==> 16 ==> 10000
總結:所以說我們可以通過&運算,進行判斷是否選擇.同理,如果題目說必須選擇,顯然&運算也可以發揮作用.
其他運算操作以后再慢慢補充吧,我們先來幾道題目感受感受.
題目選講
第一題
題目描述
求把\(N \times M\)的棋盤分割成若干個\(1 \times 2\)的的長方形,有多少種方案。
例如當\(N=2,M=4\)時,共有5種方案。當\(N=2,M=3\)時,共有3種方案。
如下圖所示:

輸入格式
輸入包含多組測試用例。
每組測試用例占一行,包含兩個整數N和M。
當輸入用例
\(N=0 M=0\)時,表示輸入終止,且該用例無需處理。
輸出格式
每個測試用例輸出一個結果,每個結果占一行。
數據范圍
輸入樣例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
輸出樣例:
1
0
1
2
3
5
144
51205
題意解析
首先這道題目題意很好理解,我就不說一句話題意了,但是我們要明確這道題目給我們的信息.
- 條件:1*2的長方形,有豎着的,也有橫着的.
- 性質:棋盤性質,數據范圍很小
- 最后答案 所有的合法方案數
通過上面給我們的信息,我們不難判斷這道題目是一道狀態壓縮動態規划算法,那么接下來我們按照上面所說的,我們現在需要處理如何二進制化狀態.
算法解析
我們發現,對於任何一個方案而言,假如說我們現在目光定格在,某一行的話,換句話說把棋盤分成兩部分,那么我們似乎會發現什么有趣的東西.

我們發現如果說我們確定了第一排的紅色,那么顯然綠色也隨着確定了.
而且如果我們確定了第一排的紅色,那么同樣的我們的第二排一部分紅色也就絕對確定了.切記不是所有的紅色.
因此我們顯然可以認為紅色為1,綠色為0,因為這道題目除了紅色擺放,就是綠色擺放了,所以說這就是這道題目二進制的狀態轉化.
狀態處理
綜上所述,我們就可以把行號看作階段.
接着我們可以設\(f[i][j]\)表示為第i行決策集合為j.(j為二進制集合,不過用十進制存儲)
那么接下來我們就要看決策的條件了.
對於\(f[i-1][k]\)轉移到\(f[i][j]\)顯然是有兩個條件的.
- j&k==0 也就是說每個數字1的下面必須是數字0,否則無法放入1*2的豎着長方形
- j|k的二進制表示中,每一段連續的0個數必須為偶數,否則無法放入1*2的橫着長方形.
代碼解法
#include <bits/stdc++.h>
using namespace std;
const int N=11;
long long f[12][1<<N];
int n,m;
bool st[1<<N];
int main()
{
ios::sync_with_stdio(false);
while(cin>>n>>m && n)
{
for(int i=0;i<1<<m;i++)//預處理,處理所有[0,2^m -1]內所有滿足二進制表示下每一段連續01都有偶數個的整數.
{
bool cnt=0,odd=0;
for(int j=0;j<m;j++)
if (i>>j & 1)//判斷第j是否選擇了
odd|=cnt,cnt=0;
else
cnt^=1;
st[i]=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++)
if ((j&k)==0 && st[j|k])//滿足上面說的條件
f[i][j]+=f[i-1][k];//轉移過來了
}
cout<<f[n][0]<<endl;//最后的結果
}
return 0;
}
