1. 程式人生 > >dp狀態壓縮

dp狀態壓縮

text 方案 格子 nes cor scan november href 壓縮

dp狀態壓縮

動態規劃本來就很抽象,狀態的設定和狀態的轉移都不好把握,而狀態壓縮的動態規劃解決的就是那種狀態很多,不容易用一般的方法表示的動態規劃問題,這個就更加的難於把握了。難點在於以下幾個方面:狀態怎麽壓縮?壓縮後怎麽表示?怎麽轉移?是否具有最優子結構?是否滿足後效性?涉及到一些位運算的操作,雖然比較抽象,但本質還是動態規劃。找準動態規劃幾個方面的問題,深刻理解動態規劃的原理,開動腦筋思考問題。這才是掌握動態規劃的關鍵。

動態規劃最關鍵的要處理的問題就是位運算的操作,容易出錯,狀態的設計也直接決定了程序的效率,或者代碼長短。狀態轉移方程一定要仔細推敲,不可一帶而過,要思考為什麽這麽做,掌握一個套路,遇見這類問題能快速的識別出問題的本質,找出狀態轉移方程和DP的邊界條件。

下面兩個題目:

poj3254 Corn Fields

Corn Fields
Time Limit: 2000MS Memory Limit: 65536K
Total Submissions: 16473 Accepted: 8678

Description

Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can‘t be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.

Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.

Input

Line 1: Two space-separated integers: M
and N
Lines 2..M+1: Line i+1 describes row i of the pasture with N space-separated integers indicating whether a square is fertile (1 for fertile, 0 for infertile)

Output

Line 1: One integer: the number of ways that FJ can choose the squares modulo 100,000,000.

Sample Input

2 3
1 1 1
0 1 0

Sample Output

9

Hint

Number the squares as follows:
1 2 3
4

There are four ways to plant only on one squares (1, 2, 3, or 4), three ways to plant on two squares (13, 14, or 34), 1 way to plant on three squares (134), and one way to plant on no squares. 4+3+1+1=9.

Source

USACO 2006 November Gold

大致題意:

給出一個n行m列的草地,1表示肥沃,0表示貧瘠,現在要把一些牛放在肥沃的草地上,但是要求所有牛不能相鄰,問你有多少種放法。

分析:

假如我們知道第 i-1 行的所有的可以放的情況,那麽對於第 i 行的可以放的一種情況,我們只要判斷它和 i - 1 行的所有情況的能不能滿足題目的所有牛不相鄰,如果有種中滿足,那麽對於 i 行的這一中情況有 x 中放法。

前面分析可知滿足子狀態,我們我們確定可以用dp來解決。

但是我們又發現,狀態是一種放法,不是我們平常dp的簡單的狀態,所以要用狀態壓縮!

但是什麽是狀態壓縮呢?

比如前面的情況,一種放法是最多由12個 0 或者 1 組成的,那麽我們很容易想到用二進制,用二進制的一個數來表示一種放法。

定義狀態dp【i】【j】,第 i 行狀態為 j 的時候放牛的種數。j 的話我們轉化成二進制,從低位到高位依次 1 表示放牛0表示沒有放牛,就可以表示一行所有的情況。

那麽轉移方程 dp【i】【j】=sum(dp【i-1】【k】)

狀態壓縮dp關鍵是處理好位運算。

這個題目用到了 & 這個運算符。

用 x & (x<<1)來判斷一個數相鄰兩位是不是同時為1,假如同時為 1 則返回一個值,否則返回 0 ,這樣就能優化掉一些狀態

用 x & y 的布爾值來判斷相同為是不是同時為1。

 1 #include <cstdio>  
 2 #include <cstring>  
 3 const int N = 13;  
 4 const int M = 1<<N;  
 5 const int mod = 100000000;  
 6 int st[M],map[M];  ///分別存每一行的狀態和給出地的狀態  
 7 int dp[N][M];  //表示在第i行狀態為j時候可以放牛的種數  
 8 bool judge1(int x)  //判斷二進制有沒有相鄰的1  
 9 {  
10     return (x&(x<<1));  
11 }  
12 bool judge2(int i,int x)  
13 {  
14     return (map[i]&st[x]);  
15 }  
16 int main()  
17 {  
18     int n,m,x;  
19     while(~scanf("%d%d",&n,&m))  
20     {  
21         memset(st,0,sizeof(st));  
22         memset(map,0,sizeof(map));  
23         memset(dp,0,sizeof(dp));  
24         for(int i=1;i<=n;i++)  
25         {  
26             for(int j=1;j<=m;j++){  
27                 scanf("%d",&x);  
28                 if(x==0)  
29                     map[i]+=(1<<(j-1));  
30             }  
31   
32         }  
33         int k=0;  
34         for(int i=0;i<(1<<m);i++){  
35             if(!judge1(i))  
36                 st[k++]=i;  
37         }  
38         for(int i=0;i<k;i++)  
39         {  
40             if(!judge2(1,i))  
41                 dp[1][i]=1;  
42         }  
43         for(int i=2;i<=n;i++)  
44         {  
45             for(int j=0;j<k;j++)  
46             {  
47                 if(judge2(i,j))  //判斷第i行 假如按狀態j放牛的話行不行。  
48                     continue;  
49                 for(int f=0;f<k;f++)  
50                 {  
51                     if(judge2(i-1,f))   //剪枝 判斷上一行與其狀態是否滿足  
52                         continue;  
53                     if(!(st[j]&st[f]))  
54                         dp[i][j]+=dp[i-1][f];  
55                 }  
56             }  
57         }  
58         int ans=0;  
59         for(int i=0;i<k;i++){  
60             ans+=dp[n][i];  
61             ans%=mod;  
62         }  
63         printf("%d\n",ans);  
64     }  
65     return 0;  
66 }  

HOJ 2662

有一個n*m的棋盤(n、m≤80,n*m≤80)要在棋盤上放k(k≤20)個棋子,使得任意兩個棋子不相鄰(每個棋子最多和周圍4個棋子相鄰)。求合法的方案總數。

直接考慮解決這個問題並不容易,我們先來考慮這個問題的退化形式:

現在我們令n=1。則我們可以很容易的想到狀態轉移方程:

設dp[i][j][0]表示當前到達第i列,一共使用了j個旗子,且當前格子的狀態為不放的狀態總數,類似的 dp[i][j][1]就是當前格子的狀態為放的狀態總數。

那麽狀態轉移方程就是

dp[i][j][0]=dp[i-1][j][1]+dp[i-1][j][0];

dp[i][j][1]=dp[i-1][j-1][0];

當n=1的時候這個問題無疑是非常簡單的,但是如果我們想模仿這種做法來解決原問題的話,就會遇到這樣的問題:如何來表示當前行的狀態?

昨天已經提到了一些狀態壓縮的知識,如果看懂了的話,應該已經明白怎麽做了。

對於每一行,如果把沒有棋子的地方記為0,有棋子的地方記為1,那麽每一行的狀態都可以表示成一個2進制數,進而將其轉化成10進制。

那麽這個問題的狀態轉移方程就變成了

設dp[ i ] [ j ][k ]表示當前到達第i列,一共使用了j個棋子,且當前行的狀態在壓縮之後的十進制數為k 時的狀態總數。那麽我們也可以類似的寫出狀態轉移方程:

dp[ i ][ j ][ k ]=sum( dp[ i-1][ j-num(k) ][ w ] ) num(k)表示 k狀態中棋子的個數,w表示前一行的狀態。

雖然寫出了狀態轉移方程,但是還是有很多細節問題需要解決:比如,如何保證當前狀態是合法的?

最基本的做法是:首先判斷k狀態是否合法,也就是判斷在這一行中是否有2個旗子相鄰,然後枚舉上一行的狀態w,判斷w狀態是否合法,然後判斷k狀態和w狀態上下之間是否有相鄰的棋子。

當然這樣做的時間復雜度是很高的,也就是說有很多地方可以優化,比如:判斷每一行狀態是否合法,可以在程序一開始判斷然後保存結果,判斷k狀態和w狀態上下之間是否有相鄰的棋子,可以利用位運算,if(k&w)說明上下之間有相鄰的棋子等等。

講到這裏,這道題目的做法已經很明確了,請大家自行完成。

①有一個1*n的棋盤(n<=80),需要在上面放置k個棋子,使棋子之間不相鄰,求方案數。 那麽這是一道很簡單的棋盤型DP。設f[i][j][0]為前i個放置j個棋子的方案數且第i位必不放,f[i][j][1]為前i個放置j個棋子的方案數且第i位必放。則存在方程: 技術分享
②有一個m*n的棋盤(n*m<=80),需要在上面放置k個棋子,使棋子之間不相鄰,求方案數。 多了一個行的狀態,這就讓人費解了——我們並不能設置一個四維方程來表示狀態。原來我們每一行只有一個格子,現在多個格子怎麽表示呢?這裏就要用到狀態壓縮了。這裏提到的狀態壓縮的方式只是其中一種,相對比較簡單的一種——二進制轉換。我們註意到題目有一個特別鬼畜的數據範圍——n*m<=80。這意味著什麽?9*9=81>80,則min(n,m)的最大值為8。我們將m,n中較小的一個看做行(易得行列轉換依舊等價),一行至多8個格子,我們令當前狀態下格子若放置了棋子則記為1,未放置記為0,那麽我們可以將一行的狀態表示為一個二進制數,繼而在狀態轉移的時候再轉換為十進制。前文提到了最多8個數,所以數組中數值最大為2^8=256。設f[i][j][k]表示當前第i列,使用了j個棋子,當前行的狀態為k(一個由二進制數轉換過來的十進制數),則狀態轉移方程為: 技術分享
其中k`表示上一行的狀態,num(k)表示在當前行狀態為k的情況下棋子的總個數。 當然我們要註意——如何判斷當前這一行與上一行是否存在相鄰的棋子?利用按位異或即可。見代碼。
 1 #include cstdio
 2 #include cstring
 3 #include algorithm
 4 using namespace std;
 5  
 6 long long f[81][1<<9][21];
 7  
 8 inline int getNum(int x)
 9 {
10         int s=0,tmp=0;
11         while (x)
12         {
13                 if (s && (x & 1)) return -1;
14                 if (s=(x & 1)) tmp++;
15                 x=x>>1;
16         }
17         return tmp;
18 }
19  
20 int main()
21 {
22         int n,m,t;
23         while (scanf("%d %d %d",&n,&m,&t)!=EOF)
24         {
25                 if (n《m)  swap(n,m);
26                 memset(f,0,sizeof(f));
27                 f[0][0][0]=1;
28                 for (int i=1;i<=n;i++)
29                         for (int r=0;r<=t;r++)
30                                 for (int j=0;j<(1<<m);j++) // 當前的狀態 
31                                 {
32                                         int num=getNum(j); // 棋子個數 
33                                         if (num==-1 || num>r) continue;
34                                         for (int k=0;k<(1<<m);k++) // 上次的狀態轉移 
35                                         {
36                                                 if (getNum(k)==-1 || k&j) continue;
37                                                 f[i][j][r]+=f[i-1][k][r-num];
38                                         }
39                                 }
40                 long long ans=0;
41                 for (int i=0;i<(1<<m);i++) ans+=f[n][i][t];
42                 printf("%lld\n",ans);
43         }
44         return 0;
45 }

參考: http://blog.csdn.net/accry/article/details/6607703 http://blog.csdn.net/y990041769/article/details/24658419 http://blog.csdn.net/lmyclever/article/details/6671923 http://blog.sina.com.cn/s/blog_6022c4720102w6jf.html

dp狀態壓縮