1. 程式人生 > >動態規劃模型總結之狀壓dp

動態規劃模型總結之狀壓dp

啊怎麼前面的都沒寫就直接狀壓了啊

狀壓比較簡單先講狀壓(其實比較向深搜的優化)

可能藍書上的例題偏難先將一些簡單的

不會位運算就gg了吧

1.問題引入

為什麼需要狀壓dp

狀壓dp可以解決一些題目的狀態隨階段增長而增長的題目,比如一個量用了還是沒用等等,把狀態壓縮成數字存入陣列然後進行dp

啊聽著好抽象啊

那我們搞一個例題看看

狀壓dp入門題:玉米田

簡化後的題目意思是有一塊矩陣,可以取一些1,但是不能有相鄰的1,問有多少種取法

此題是否可以 d

f s dfs ?應該是可以的,但是會TLE

我們發現在 d f s dfs 的是後需要記錄當前位置取沒取,這樣才能判斷該位置可不可以去

這提示我們可以使用狀壓 d p dp

我們首先預處理出每一行有哪些取的方案合法,即不能取0也不能有相鄰

不能取0比較簡單,也就是把每行的草地情況壓成int(存到 f f

陣列),然後如果列舉到一個狀態 s t a sta ,如何判斷這個 s t a sta 沒有零?就是他與該行的 f f 的且是否還是原數,因為如果不是原數,說明他選取了一些0,否則肯定是合法的

那如何判斷一個狀態沒有相鄰的?這個就要考察位運算的功底了,其實就是左移一位和右移一位都與原數沒有交

預處理出這些就可以進入最後的 d p dp 環節

d p [ i ] [ s t a ] dp[i][sta] 表示第 i i 行現在的狀態為 s t a sta

然後列舉之前的狀態為 m a s k mask ,如果 s t a sta m a s k mask 沒有交,則把 d p [ i 1 ] [ m a s k ] dp[i-1][mask] 的值加到 d p [ i ] [ s t a ] dp[i][sta] 裡面去

答案就是 d p [ n ] [ s t a ] \sum{}dp[n][sta]

我們發現如果第 i i 行的取法只與 i 1 i-1 行和他自己有關,於是可以把 i i 這一維滾動掉

時間複雜度 O ( n 2 n 2 n ) O(n*2^n*2^n)

n n 才12,正好

const int p = 100000000 ;
int dp[13][5000],a[13][13],f[13],ok[5000];
int n,m ;
int main(){
    scanf("%d%d",&n,&m) ;
    for (int i=1;i<=n;i++)
    for (int j=1;j<=m;j++)
    scanf("%d",&a[i][j]) ;
    for (int i=1;i<=n;i++)
    for (int j=1;j<=m;j++)
    f[i]=(f[i]<<1)+a[i][j] ;
    for (int i=0;i<(1<<m);i++) ok[i]=((!(i&(i<<1)))&&(!(i&(i>>1)))) ;
    dp[0][0]=1 ;
    for (int i=1;i<=n;i++){
        for (int j=0;j<(1<<m);j++) {
            if (ok[j] && ((j&f[i])==j)){
                for (int k=0;k<(1<<m);k++)
                if (!(j&k)){
                    dp[i][j]=(dp[i][j]+dp[i-1][k])%p ;
                }
            }
        }
    }
    int ans=0;
    for (int i=0;i<(1<<m);i++) ans=(ans+dp[n][i])%p ;
    printf("%d\n",ans) ;
} 

如果您覺得這個題目還比較困難,不妨先做一個小練習:

Water

題目描述

n n 個裝著水的開水瓶,要把它們中的水彙總到不超過 k k 個開水瓶裡,把第 i i 個開水瓶裡的水全都倒到第 j j 個開水瓶裡(無論它們現在裝了多少水)的代價是 c i j c_{ij} 。求最小總代價。

輸入格式

第一行兩個整數 n , k n,k

接下來的 n n 行,每行 n n 個整數表示 c i j c_{ij} ,保證 c i i = 0 c_{ii}=0

輸出格式

輸出一行一個整數表示答案。

樣例

input

5 2
0 5 4 3 2
7 0 4 4 4
3 3 0 1 2
4 3 1 0 5
4 5 5 5 0 

output

5

資料範圍

1 k n 20 , c i j 1 0 5 1≤k≤n≤20,c_{ij}≤10^5

如果您上一個題目聽懂了,這個就是小case了

d p [ m a s k ] dp[mask] 表示瓶子狀態為 m a s k mask 最小需要花費的代價

然後列舉把 i i 號瓶子的水轉移到 j j 號瓶子就好了

然後如果中間1的個數 &lt; = k &lt;=k 的話就更新答案

int n, k, ans = iinf ;
int dp[N], a[25][25] ;

int calc(int x) {
	int res = 0 ;
	while (x) {
		x = x & x - 1 ;
		res++ ;
	}
	return res ;
}

signed main(){
	scanf("%d%d", &n, &k) ;
	if (n == k) print(0) ;
	for (int i = 0; i < n; i++)
	for (int j = 0; j < n; j++)
	scanf("%d", &a[i][j]) ;
	for (int i = 0; i <= (1 << n) - 1; i++) dp[i] = iinf ;
	dp[(1 << n) - 1] = 0 ;
	for (int s = (1 << n) - 1; s >= 0; s--) { // 列舉當前狀態
		if (calc(s) <= k) ans = min(ans, dp[s]) ;
		for (int i = 0; i < n; i++) // 把第i個水壺中的水
		if (s & (1 << i))
		for (int j = 0; j < n; j++) // 轉移到第j個水壺中
		if (i != j) {
			dp[(s - (1 << i)) | (1 << j)] = min(dp[(s - (1 << i)) | (1 << j)], dp[s] + a[i][j]) ;
		}
	}
	printf("%d\n", ans) ;
}