DP專題-學習筆記:狀態壓縮 DP
1. 前言
狀態壓縮 DP,簡稱狀壓 DP,是一種 DP (廢話)。
這種 DP 的特點就是通常與二進位制有關(當然也可能是其他進位制),通常複雜度為 2 的階乘次級別。
狀壓 DP 的問題有兩個鮮明的特徵:
- 問題的資料規模特別小,2 的階乘次可以通過。
- 題目通常都是選與不選兩種選擇,可以使用二進位制串表示。
第 2 個是什麼意思呢?
打個比方,現在有 5 盤菜在你面前,編號 1-5,你此時想吃 1,3,4 號菜,那麼就可以將你做的選擇表示為 10110 的二進位制串。
在繼續看下去之前,請先確保熟練掌握各種位運算的知識。不瞭解的讀者可以參考
如果沒有特殊說明,本文的所有 01 串全部視為二進位制串。
2. 詳解
對於狀壓 DP,尤其重要的一點是:搞清楚二進位制串表示的意義是什麼。
通常來講,題中只有兩種選擇的物品就可以使用二進位制串來表示。
比如這道題,對於每一行而言:每一個格子只可能放或不放,那麼此時就可以用二進位制串表示。
例如 \(n=6\) 時,101001 表示第 1,3,6 列放,其餘列不放。
那麼首先我們需要一個 dfs
函式確定所有二進位制串。
需要注意的是,在 dfs
中,為了接下來處理方便,我們需要儘可能的將題中限制條件加入 dfs
中以減少狀態數和後面 DP 時的非法狀態判斷。
對於這道題,我們能夠完成的就是在 dfs
的時候提前將相鄰兩項均為 1 的二進位制串過濾掉。
程式碼:
void dfs(int pos, int num, int one)
{
if (pos >= n) {State[++cnt] = num; sum[cnt] = one; return ;}
dfs(pos + 1, num, one);
dfs(pos + 2, num + (1 << pos), one + 1);
}
其中 \(State_i\) 表示第 \(i\) 個狀態對應的二進位制串,\(sum_i\) 表示第 \(i\) 個二進位制串用了多少個 1。
現在已經知道了所有單行內合法的二進位制串,那麼接下來開始設計 DP。
設 \(f_{i,j,k}\) 表示在第 \(i\) 行,\(1-i\) 行已經使用了 \(j\) 個 1,當前行的狀態為 \(State_k\) 的方案數。
需要注意的是:
- \(j\) 個 1 實際上就是 \(j\) 個國王。
- 注意第三位的 \(k\) 只是一個標號,真正的狀態是 \(State_k\),這樣做是因為 \(State_k\) 可能很大,防止炸空間。
- \(j \geq sum_k\)
那麼怎麼轉移呢?
對於第 \(i\) 行的轉移,我們需要知道這樣 3 個訊息:
- 當行二進位制串 \(State_j\)。
- 上一行二進位制串 \(State_k\)。
- 上一行至第一行用了 \(l\) 個 1。
但是考慮到一個國王可能會影響到上面一行的拜訪,我們需要過濾掉這些非法情況。
分為 3 種情況:
- 如下。
這種情況下為上一行國王在這一行國王正上方,判斷方法為 \(j \& k\)k:....1.... j:....1....
- 如下。
這種情況下為上一行國王在這一行國王左上角,判斷方法為 \((j<<1)\&k\),利用右移運算轉化成第 1 種情況。k:...1..... j:....1....
- 如下。
這種情況下為上一行國王在這一行國王右上角,判斷方法為 \((j>>1)\&k\),同樣利用左移運算轉化成第 1 種情況。k:.....1... j:....1....
當上面三條有任意一條符合,即為非法狀態。
這樣過濾了所有非法狀態之後,就可以愉快的轉移啦!
轉移方程如下:
\[f_{i,l+sum_j,j}=\sum f_{i-1,l,k}(l \geq sum_k) \]其中 \(j,k\) 為合法狀態。
最後答案為 \(\sum f_{n,k,i}|i \in [1,cnt]\)。
初值:\(f_{1,sum_i,i}=1|i \in [1,cnt]\)。
程式碼:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MAXN = 9 + 5, MAXP = (1 << 9) + 10, MAXK = 9 * 9 + 10;
int n, p, State[MAXP], sum[MAXP], cnt;
LL f[MAXN][MAXK][MAXP];
int read()
{
int sum = 0, fh = 1; char ch = getchar();
for (; ch < '0' || ch > '9' ; ch = getchar()) fh -= (ch == '-') << 1;
for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
return (fh == 1) ? sum : -sum;
}
void dfs(int pos, int num, int one)
{
if (pos >= n) {State[++cnt] = num; sum[cnt] = one; return ;}
dfs(pos + 1, num, one);
dfs(pos + 2, num + (1 << pos), one + 1);
}
int main()
{
n = read(), p = read();
dfs(0, 0, 0);
for (int i = 1; i <= cnt; ++i) f[1][sum[i]][i] = 1;
for (int i = 2; i <= n; ++i)
for (int j = 1; j <= cnt; ++j)
for (int k = 1; k <= cnt; ++k)
{
if (State[j] & State[k]) continue;
if ((State[j] << 1) & State[k]) continue;
if ((State[j] >> 1) & State[k]) continue;
for (int l = sum[j]; l <= p; ++l) f[i][l + sum[k]][k] += f[i - 1][l][j];
}
LL ans = 0;
for (int i = 1; i <= cnt; ++i) ans += f[n][p][i];
printf("%lld\n", ans);
return 0;
}
3. 關於空間
狀壓 DP 很要命的一點就是空間限制。
根據理論推算,上面這道題的狀態數為 \(2^n\),這道題還好,但是在一些別的題目中就會 MLE。例子見『練習題』的博文的第一道練習題。連結後面有。
那麼此時怎麼辦呢?
可以採用滾動陣列的方式減小空間,比如第一道練習題,洛谷題解區絕大多數人都是採用滾動陣列節省空間,但是其實有一種更好的方法。
將資料調到最大,看看有幾個狀態不是就好了?
比如上面這道題,當 \(n=9\) 時,狀態數只有 89 個,完全不用開 \(2^n\) 空間,開 \(89+10\) 即可。
這個方法在卡狀壓 DP 的空間的時候尤其有用!
當然對於某些毒瘤題,兩種方法都要用。
4. 練習題
練習題傳送門:DP演算法總結&專題訓練3(狀壓 DP)