dp状态压缩
dp状态压缩
动态规划本来就很抽象,状态的设定和状态的转移都不好把握,而状态压缩的动态规划解决的就是那种状态很多,不容易用一般的方法表示的动态规划问题,这个就更加的难于把握了。难点在于以下几个方面:状态怎么压缩?压缩后怎么表示?怎么转移?是否具有最优子结构?是否满足后效性?涉及到一些位运算的操作,虽然比较抽象,但本质还是动态规划。找准动态规划几个方面的问题,深刻理解动态规划的原理,开动脑筋思考问题。这才是掌握动态规划的关键。
动态规划最关键的要处理的问题就是位运算的操作,容易出错,状态的设计也直接决定了程序的效率,或者代码长短。状态转移方程一定要仔细推敲,不可一带而过,要思考为什么这么做,掌握一个套路,遇见这类问题能快速的识别出问题的本质,找出状态转移方程和DP的边界条件。
下面两个题目:
poj3254 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
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
Sample Input
2 3 1 1 1 0 1 0
Sample Output
9
Hint
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
大致题意:
给出一个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 #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 }