1. 程式人生 > 其它 >DP專題-學習筆記:狀態壓縮 DP

DP專題-學習筆記:狀態壓縮 DP

目錄

1. 前言

狀態壓縮 DP,簡稱狀壓 DP,是一種 DP (廢話)

這種 DP 的特點就是通常與二進位制有關(當然也可能是其他進位制),通常複雜度為 2 的階乘次級別。

狀壓 DP 的問題有兩個鮮明的特徵:

  1. 問題的資料規模特別小,2 的階乘次可以通過。
  2. 題目通常都是選與不選兩種選擇,可以使用二進位制串表示。

第 2 個是什麼意思呢?

打個比方,現在有 5 盤菜在你面前,編號 1-5,你此時想吃 1,3,4 號菜,那麼就可以將你做的選擇表示為 10110 的二進位制串。

在繼續看下去之前,請先確保熟練掌握各種位運算的知識。不瞭解的讀者可以參考

OI-wiki:位運算

如果沒有特殊說明,本文的所有 01 串全部視為二進位制串。

2. 詳解

例題:P1896 [SCOI2005]互不侵犯

對於狀壓 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\) 的方案數。

需要注意的是:

  1. \(j\) 個 1 實際上就是 \(j\) 個國王。
  2. 注意第三位的 \(k\) 只是一個標號,真正的狀態是 \(State_k\),這樣做是因為 \(State_k\) 可能很大,防止炸空間。
  3. \(j \geq sum_k\)

那麼怎麼轉移呢?

對於第 \(i\) 行的轉移,我們需要知道這樣 3 個訊息:

  1. 當行二進位制串 \(State_j\)
  2. 上一行二進位制串 \(State_k\)
  3. 上一行至第一行用了 \(l\) 個 1。

但是考慮到一個國王可能會影響到上面一行的拜訪,我們需要過濾掉這些非法情況。

分為 3 種情況:

  1. 如下。
    k:....1....
    j:....1....
    
    這種情況下為上一行國王在這一行國王正上方,判斷方法為 \(j \& k\)
  2. 如下。
    k:...1.....
    j:....1....
    
    這種情況下為上一行國王在這一行國王左上角,判斷方法為 \((j<<1)\&k\),利用右移運算轉化成第 1 種情況。
  3. 如下。
    k:.....1...
    j:....1....
    
    這種情況下為上一行國王在這一行國王右上角,判斷方法為 \((j>>1)\&k\),同樣利用左移運算轉化成第 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)