微信扫一扫打赏支持

dp状态压缩

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位必放。则存在方程:
                           [知识点]状态压缩DP
       ②有一个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(一个由二进制数转换过来的十进制数),则状态转移方程为:
                [知识点]状态压缩DP
            其中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
posted @ 2017-08-24 23:03  范仁义  阅读(511)  评论(0编辑  收藏  举报