原文地址:点击打开链接
动态规划本来就很抽象,状态的设定和状态的转移都不好把握,而状态压缩的动态规划解决的就是那种状态很多,不容易用一般的方法表示的动态规划问题,这个就更加的难于把握了。难点在于以下几个方面:状态怎么压缩?压缩后怎么表示?怎么转移?是否具有最优子结构?是否满足后效性?涉及到一些位运算的操作,虽然比较抽象,但本质还是动态规划。找准动态规划几个方面的问题,深刻理解动态规划的原理,开动脑筋思考问题。这才是掌握动态规划的关键。
动态规划最关键的要处理的问题就是位运算的操作,容易出错,状态的设计也直接决定了程序的效率,或者代码长短。状态转移方程一定要仔细推敲,不可一带而过,要思考为什么这么做,掌握一个套路,遇见这类问题能快速的识别出问题的本质,找出状态转移方程和DP的边界条件。
下面是一些关于状态压缩DP的题目,大都不难。状压DP的东西还有很多,还会接着总结。
【POJ3254】Corn Fields
【题目大意】一个矩阵里有很多格子,每个格子有两种状态,可以放牧和不可以放牧,可以放牧用1表示,否则用0表示,在这块牧场放牛,要求两个相邻的方格不能同时放牛,即牛与牛不能相邻。问有多少种放牛方案(一头牛都不放也是一种方案)
【解析】根据题意,把每一行的状态用二进制的数表示,0代表不在这块放牛,1表示在这一块放牛。首先很容易看到,每一行的状态要符合牧场的硬件条件,即牛必须放在能放牧的方格上。这样就能排除一些状态。另外,牛与牛之间不能相邻,这样就要求每一行中不能存在两个相邻的1,这样也能排除很多状态。然后就是根据上一行的状态转移到当前行的状态的问题了。必须符合不能有两个1在同一列(两只牛也不能竖着相邻)的条件。这样也能去掉一些状态。然后,上一行的所有符合条件的状态的总的方案数就是当前行该状态的方案数。
【状态表示】dp[state][i]:在状态为state时,到第i行符合条件的可以放牛的方案数
【状态转移方程】dp[state][i] =Sigma dp[state'][i-1] (state'为符合条件的所有状态)
【DP边界条件】首行放牛的方案数dp[state][1] =1(state符合条件) OR 0 (state不符合条件)
【代码】
- #include <cstdio>
- #include <cstring>
- using namespace std;
- #define mod 100000000
- int M,N,top = 0;
- int state[600],num[110];
- int dp[20][600];
- int cur[20];
- inline bool ok(int x){
- if(x&x<<1)return 0;
- return 1;
- }
- void init(){
- top = 0;
- int total = 1 << N;
- for(int i = 0; i < total; ++i){
- if(ok(i))state[++top] = i;
- }
- }
- inline bool fit(int x,int k){
- if(x&cur[k])return 0;
- return 1;
- }
- inline int jcount(int x)
- {
- int cnt=0;
- while(x)
- {
- cnt++;
- x&=(x-1);
- }
- return cnt;
- }
-
- int main(){
- while(scanf("%d%d",&M,&N)!= EOF){
- init();
- memset(dp,0,sizeof(dp));
- for(int i = 1; i <= M; ++i){
- cur[i] = 0;
- int num;
- for(int j = 1; j <= N; ++j){
- scanf("%d",&num);
- if(num == 0)cur[i] +=(1<<(N-j));
- }
- }
- for(int i = 1;i <= top;i++){
- if(fit(state[i],1)){
- dp[1][i] = 1;
- }
- }
- for(int i = 2; i <= M; ++i){
- for(int k = 1; k <= top; ++k){
- if(!fit(state[k],i))continue;
- for(int j = 1; j <= top ;++j){
- if(!fit(state[j],i-1))continue;
- if(state[k]&state[j])continue;
- dp[i][k] = (dp[i][k] +dp[i-1][j])%mod;
- }
- }
- }
- int ans = 0;
- for(int i = 1; i <= top; ++i){
- ans = (ans + dp[M][i])%mod;
- }
- printf("%d\n",ans);
- }
- }
【POJ1185】炮兵阵地--经典状压DP
【题目大意】类似于上面一道题,一个方格组成的矩阵,每个方格可以放大炮用0表示,不可以放大炮用1表示(原题用字母),让放最多的大炮,大炮与大炮间不会互相攻击。
【解析】可以发现,对于每一行放大炮的状态,只与它上面一行和上上一行的状态有关,每一行用状态压缩的表示方法,0表示不放大炮,1表示放大炮,同样的,先要满足硬件条件,即有的地方不能放大炮,然后就是每一行中不能有两个1的距离小于2(保证横着不互相攻击),这些要预先处理一下。然后就是状态表示和转移的问题了,因为是和前两行的状态有关,所以要开个三维的数组来表示状态,当前行的状态可由前两行的状态转移而来。即如果当前行的状态符合前两行的约束条件(不和前两行的大炮互相攻击),则当前行的最大值就是上一个状态的值加上当前状态中1的个数(当前行放大炮的个数)
【状态表示】dp[i][j][k] 表示第i行状态为k,第i-1状态为j时的最大炮兵个数。
【状态转移方程】dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]); num[t]为t状态中1的个数
【DP边界条件】dp[1][1][i] =num[i] 状态i能够满足第一行的硬件条件(注意:这里的i指的是第i个状态,不是一个二进制数,开一个数组保存二进制状态)
【代码】
- #include <cstdio>
- #include <cstring>
- using namespace std;
- #define max(a,b) (a) > (b) ? (a) : (b)
-
- int N,M;
- char map[110][20],num[110],top;
- int stk[70],cur[110];
- int dp[110][70][70];
-
- inline bool ok(int x){
- if(x&(x<<1)) return 0;
- if(x&(x<<2)) return 0;
- return 1;
- }
-
- inline void jinit()
- {
- top=0;
- int i,total=1<<N;
- for(i=0;i<total;i++) if(ok(i)) stk[++top]=i;
- }
-
- inline bool fit(int x,int k)
- {
- if(cur[k]&x) return 0;
- return 1;
- }
-
- inline int jcount(int x)
- {
- int cnt=0;
- while(x)
- {
- cnt++;
- x&=(x-1);
- }
- return cnt;
- }
-
- int main(){
- while(scanf("%d%d",&M,&N) != EOF){
- if(N == 0 && M == 0)break;
- jinit();
- for(int i = 1; i <= M; ++i)scanf("%s",map[i]+1);
- for(int i = 1; i <= M; ++i)
- for(int j = 1; j <= N; ++j){
- cur[i]=0;
- for(j=1;j<=N;j++){
- if(map[i][j]=='H')cur[i]+=(1<<(j-1));
- }
- }
- memset(dp,-1,sizeof(dp));
-
-
- for(int i = 1;i <= top;i++){
- num[i]=jcount(stk[i]);
- if(fit(stk[i],1))
- dp[1][1][i]=num[i];
- }
- int i,t,j,k;
- for(i = 2;i <= M;i++){
- for(t = 1;t <= top;t++){
- if(!fit(stk[t],i)) continue;
- for(j = 1;j <= top;j++)
- {
- if(stk[t]&stk[j])continue;
- for(k = 1;k <= top;k++)
- {
- if(stk[t]&stk[k])continue;
- if(dp[i-1][j][k]==-1)continue;
- dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]);
- }
- }
- }
- }
- int ans = 0;
- for(i = 1; i <= M; ++i)
- for(j = 1; j <= top; ++j)
- for(k = 1; k <= top; ++k)
- ans = max(ans,dp[i][j][k]);
- printf("%d\n",ans);
- }
- return 0;
- }
【POJ3311】Hie With The Pie
【题目大意】类似于TSP问题,只是每个点可以走多次,比经典TSP问题不同的是要先用弗洛伊的预处理一下两两之间的距离。求最短距离。
【解析】可以用全排列做,求出一个最短的距离即可。或者用状态压缩DP.用一个二进制数表示城市是否走过
【状态表示】dp[state][i]表示到达i点状态为state的最短距离
【状态转移方程】dp[state][i] =min{dp[state][i],dp[state'][j]+dis[j][i]} dis[j][i]为j到i的最短距离
【DP边界条件】dp[state][i] =dis[0][i] state是只经过i的状态
【代码】
- #include<iostream>
- #define INF 100000000
- using namespace std;
- int dis[12][12];
- int dp[1<<11][12];
- int n,ans,_min;
- int main()
- {
-
- while(scanf("%d",&n) && n)
- {
- for(int i = 0;i <= n;++i)
- for(int j = 0;j <= n;++j)
- scanf("%d",&dis[i][j]);
- for(int k = 0;k <= n;++k)
- for(int i = 0;i <= n;++i)
- for(int j = 0;j <=n;++j)
- if(dis[i][k] + dis[k][j]< dis[i][j])
- dis[i][j] = dis[i][k] +dis[k][j];
-
- for(int S = 0;S <= (1<<n)-1;++S)
- for(int i = 1;i <= n;++i)
- {
- if(S & (1<<(i-1)))
- {
- if(S ==(1<<(i-1))) dp[S][i] =dis[0][i];
- else
- {
- dp[S][i] = INF;
- for(int j = 1;j <=n;++j)
- {
- if(S &(1<<(j-1)) && j != i)
- dp[S][i] =min(dp[S^(1<<(i-1))][j] + dis[j][i],dp[S][i]);
-
- }
- }
- }
- }
- ans = dp[(1<<n)-1][1] + dis[1][0];
- for(int i = 2;i <= n;++i)
- if(dp[(1<<n)-1][i] + dis[i][0] < ans)
- ans = dp[(1<<n)-1][i] +dis[i][0];
- printf("%d\n",ans);
- }
- return 0;
- }
【HDU3001】Traveling
【题目大意】10个点的TSP问题,但是要求每个点最多走两边,不是只可以走一次,所以要用三进制的状态压缩解决这个问题。可以预处理每个状态的第k位是什么。
【解析】和tsp问题相同,类似于上面那个题
【状态表示】【状态转移方程】同上题,具体见代码
【代码】
- #include <cstdio>
- #include <cstring>
- #define INF 0x1f1f1f1f //刚发现这里写0x1f1f1f跑的比0x1f1f1f1f差不多慢了一倍!Orz~
- #define min(a,b) (a) < (b) ? (a) : (b)
- using namespace std;
-
- int N,M;
- int tri[12] ={0,1,3,9,27,81,243,729,2187,6561,19683,59049};
- int dig[59050][11];
- int edge[11][11],dp[59050][11];
-
- int main(){
- for(int i = 0; i < 59050; ++i){
- int t = i;
- for(int j = 1; j <= 10; ++j){
- dig[i][j] = t%3;
- t /= 3;
- if(t == 0)break;
- }
- }
-
- while(scanf("%d%d",&N,&M) != EOF){
- memset(edge,INF,sizeof(edge));
-
- int a,b,c;
- while(M --){
- scanf("%d%d%d",&a,&b,&c);
- if(c < edge[a][b])edge[a][b] = edge[b][a] = c;
- }
-
- memset(dp,INF,sizeof(dp));
-
- for(int i = 1; i <= N; ++i)dp[tri[i]][i] = 0;
- int ans = INF;
- for(int S = 0; S < tri[N+1]; ++S){
- int visit_all = 1;
- for(int i = 1; i <= N; ++i){
- if(dig[S][i] == 0)visit_all = 0;
- if(dp[S][i] == INF)continue;
-
- for(int j = 1; j <= N; ++j){
- if(i == j)continue;
- if(edge[i][j] == INF ||dig[S][j] >= 2)continue;
- int newS = S + tri[j];
- dp[newS][j] =min(dp[newS][j],dp[S][i] + edge[i][j]);
- }
- }
- if(visit_all){
- for(int j = 1; j <= N; ++j)
- ans = min(ans,dp[S][j]);
- }
-
- }
- if(ans == INF){
- puts("-1");
- continue;
- }
- printf("%d\n",ans);
- }
- return 0;
- }
【POJ2288】Islands and Bridge
【题目大意】求汉密尔顿的一道变形问题,中间每个点有权值,关于最后得分的描述如下
Suppose there are n islands. The value of aHamilton path C1C2...Cn is calculated as the sum of three parts. Let Vi be thevalue for the island Ci. As the first part, we sum over all the Vi values foreach island in the path. For the second part, for each edge CiCi+1 in the path,we add the product Vi*Vi+1. And for the third part, whenever three consecutiveislands CiCi+1Ci+2 in the path forms a triangle in the map, i.e. there is abridge between Ci and Ci+2, we add the product Vi*Vi+1*Vi+2.
这题要求让得分最高
【解析】发现每个点的状态由前面两个点确定,用DP(S,A,B)表示状态为S时,当前到达A,而上一个点是B时的最大得分,这个状态由DP(S',B,C)通过从B走到A得到,S'=S-(1<<A),即S'状态就是经过B和C但不经过A的一个状态,C是不同于A和B的一个点。
【状态转移】dp[S][A][B] =max(dp[S][A][B],dp[S'][B][C]+temp) 这里的temp指的是加上的得分即Vb*Va+Va,如果构成三角关系(即A和C间有边),temp就要再加上Vb*Va*Vc.
【边界条件】DP((1<<A)+(1<<B),A,B)=Va+Vb+Va*Vb(A和B间有边)表示
【代码】
- #include <cstdio>
- #include <cstring>
- using namespace std;
- const int MAXN = 13;
- const int MAX_S = 1<<(MAXN+1);
- long long dp[MAX_S][MAXN+1][MAXN+1];
- long long way[MAX_S][MAXN+1][MAXN+1];
- int edge[MAXN+1][MAXN+1];
- long long V[MAXN+1];
-
- int N,M;
- int main(){
- int cas;
- scanf("%d",&cas);
- while(cas --){
- memset(edge,0,sizeof(edge));
- scanf("%d%d",&N,&M);
- for(int i = 1; i <= N; ++i)
- scanf("%d",&V[i]);
- if(N == 1){
- printf("%d 1\n",V[1]);
- continue;
- }
- int a,b;
- while(M --){
- scanf("%d%d",&a,&b);
- edge[a][b] = edge[b][a] = 1;
- }
-
- memset(dp,-1,sizeof(dp));
- memset(way,0,sizeof(way));
- int ii,jj;
- long long temp;
- for(int i = 1; i <= N; ++i)
- for(int j = 1; j <= N; ++j){
- if(i == j || !edge[i][j])continue;
- ii = 1<<(i-1);
- jj = 1<<(j-1);
- temp = V[i]+V[j]+V[i]*V[j];
- dp[ii+jj][i][j] = temp;
- way[ii+jj][i][j] = 1;
- }
-
- for(int S = 0; S < (1<<N); ++S)
- for(int i = 1; i <= N; ++i){
- if((S&(1<<(i-1))) == 0)continue;
- for(int j = 1; j <= N; ++j){
- if((S&(1<<(j-1))) ==0 || i == j || !edge[i][j])continue;
- for(int k = 1; k <= N; ++k){
- if(i == k || j == k ||(S&(1<<(k-1))) == 0)continue;
- int newS = S -(1<<(i-1));
- if(dp[newS][j][k] ==-1)continue;
- if(!edge[j][k])continue;
-
- temp =V[i]+V[i]*V[j]+dp[newS][j][k];
- if(edge[i][k])temp +=V[i]*V[j]*V[k];
- if(dp[S][i][j] < temp){
- dp[S][i][j] = temp;
- way[S][i][j] =way[newS][j][k];
- }
-
- else if(temp ==dp[S][i][j])way[S][i][j] += way[newS][j][k];
- }
- }
- }
- long long ans = -1,num = 0;
- int p = (1<<(N)) - 1;
- for (int i = 1; i <= N; ++i)
- for (int j = 1; j <= N; ++j){
- if(i == j)continue;
- if (ans < dp[p][i][j]){
- ans = dp[p][i][j];
- num = way[p][i][j];
- }
- else if (ans == dp[p][i][j])
- num += way[p][i][j];
- }
- if(ans == -1){
- puts("0 0");
- continue;
- }
- printf("%lld %lld\n",ans,num/2);
- }
- return 0;
- }
【ZOJ4257】MostPowerful
【题目大意】不超过10种气体,两两之间相互碰撞可以产生一定的能量,如a碰b,那么b气体就消失,自身不能碰自身,问最后所能得到的最大能量。
【题目解析】用10位二进制表示气体是否存在,0表示存在,1表示不存在,S(上一个状态)中的两种气体碰撞并且有一种消失,可以得到newS的状态(状态转移)
【状态表示】dp[state] 状态为state时的最大能量
【转移方程】dp[state] = max(dp[state],dp[state']+a[i][j])
【边界条件】dp[i] = 0;
【代码】
- #include <cstdio>
-
- #include <cstring>
-
- using namespace std;
-
- #define max(a,b) (a) > (b) ? (a) : (b)
-
- const int MAXN = 10;
-
- const int MAX_S = 1 << 10;
-
- int a[MAXN+1][MAXN+1];
-
- int dp[MAX_S];
-
- int N;
-
- int main(){
-
- while(scanf("%d",&N) != EOF){
-
- if(N == 0)break;
-
- for(int i = 0; i < N; ++i)
-
- for(int j = 0; j < N; ++j){
-
- scanf("%d",&a[i][j]);
-
- }
-
- memset(dp,0,sizeof(dp));
-
- int full = 1 << N;
-
- for(int s = 0; s < full; ++s){
-
- for(int i = 0; i < N; ++i){
-
- if((s&(1<<i)))continue;
-
- for(int j = 0; j < N; ++j){
-
- if(i == j)continue;
-
- if( (s&(1<<j)) )continue;
-
- int newS = s + (1<<j);
-
- dp[newS] = max(dp[newS],dp[s] + a[i][j]);
-
- }
-
-
-
- }
-
- }
-
- int ans = 0;
-
- for(int s = 0; s < full ; ++s)
-
- ans = max(ans,dp[s]);
-
- printf("%d\n",ans);
-
- }
-
- return 0;
-
- }
【POJ2411】Mondriaan'sDream
【题目大意】一个矩阵,只能放1*2的木块,问将这个矩阵完全覆盖的不同放法有多少种。
【解析】如果是横着的就定义11,如果竖着的定义为竖着的01,这样按行dp只需要考虑两件事儿,当前行&上一行,是不是全为1,不是说明竖着有空(不可能出现竖着的00),另一个要检查当前行里有没有横放的,但为奇数的1。
【状态表示】dp[state][i]第i行状态为state时候的方案数
【转移方程】dp[state][i] += dp[state'][i-1] state'为i-1行的,不与i行状态state冲突的状态
【边界条件】第一行 符合条件的状态记为1 即dp[state][0] = 1;
【代码】
【HDU3681】PrisonBreak 状态压缩DP+BFS+二分答案
2010杭州赛区的题目,以现在的水平遇到这种题也就能想一下,赛场上动手写这个题是不会的。前天做状压DP的时候又看到这个题,没想起来怎么做,昨天看了一下解题报告开始下手,遇到了各种问题。调试了N久,终于过了。
【题目大意】机器人从F出发,走到G可以充电,走到Y关掉开关,D不能走进,要求把所有开关关掉,且电量最少,并求出该最小电量。
【题目解析】机器人从出发点出发要求走过所有的Y,因为点很少,所以就能想到经典的TSP问题(起初我也想到了),但关于G点(不要YY)能充电的问题不知道怎么办,看了下解题报告才知道。G点可以充电,到达G点就把当前能量更新为电池容量然后继续走。因为每个G点只能充一次电,这就好像TSP中的每个点只能走一次一样(G和Y都可以走多次,但走到G充电后,该点就变为了S,而走到Y关上开关以后,Y也变成了S。这是一个很巧妙地想法,所以要求Y点只能关一次开关,G点只能充一次电,这就是TSP了。Orz赛场上可以秒杀这题的大神们),然后就是二分答案了,用状压DP判定当前电池容量的情况下是否能符合条件。
【状态表示】dp[s][i]表示到达当前i点状态为s时最大的剩余的能量
【转移方程】同TSP问题了
【边界条件】dp[1<<sid][sid] = rongliang.即出发点的能量就是电池容量
【代码】:
- #include <cstdio>
-
- #include <cstring>
-
- #include <cmath>
-
- #include <queue>
-
- using namespace std;
-
- #define INF 0x1f1f1f1f
-
- int dp[32769][16];
-
- int dist[16][16][16][16];
-
- int di[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
-
- int M,N,sid,nCnt,FinalState;
-
- char map[16][16];
-
- struct node{
-
- int x,y;
-
- node(){}
-
- node(int _x,int _y):x(_x),y(_y){}
-
- }nodes[16];
-
-
-
- inline void BFS(node start)
-
- {
-
- queue<node> que;
-
- int sx = start.x,sy = start.y;
-
- dist[sx][sy][sx][sy] = 0;
-
- que.push(start);
-
- node cur;
-
- while(!que.empty()){
-
- cur = que.front();
-
- que.pop();
-
- int x = cur.x,y = cur.y,tx,ty;
-
- for(int i = 0; i < 4; ++i){
-
- tx = x + di[i][0];
-
- ty = y + di[i][1];
-
- if(tx < 0 || tx >= M || ty < 0 || ty >= N || map[tx][ty] == 'D')continue;
-
- if(dist[sx][sy][tx][ty] == -1){
-
- dist[sx][sy][tx][ty] = dist[sx][sy][x][y] + 1;
-
- que.push(node(tx,ty));
-
- }
-
- }
-
- }
-
- }
-
- inline bool ok(int s,int t){
- if(((s&t)&t) == t)return 1;
- return 0;
- }
- inline bool check(int step){
- int res = -1;
- memset(dp,-1,sizeof(dp));
- dp[1<<sid][sid] = step;
- int full = 1<<nCnt;
- for(int s = 0; s < full; ++s){
-
- for(int i = 0; i < nCnt; ++i){
- if((s&(1<<i)) == 0 || dp[s][i] == -1)continue;
- if(ok(s,FinalState))res = max(res,dp[s][i]);
- for(int j = 0; j < nCnt; ++j){
- int temp = dist[nodes[i].x][nodes[i].y][nodes[j].x][nodes[j].y];
- if(i == j || temp == -1 || (s&(1<<j)))continue;
- temp = dp[s][i] - temp;
- if(temp < 0)continue;
- int newS = s + (1<<j);
- dp[newS][j] = max(dp[newS][j],temp);
- if(map[nodes[j].x][nodes[j].y] == 'G')dp[newS][j] = step;
-
- }
-
- }
-
- }
-
- if(res < 0)return 0;
-
- return 1;
-
- }
-
-
-
- inline int solve(){
-
- int low = 0,high = 300;
-
- int mid;
-
- while(low <= high){
-
- mid = (low+high)/2;
-
- if(check(mid))high = mid-1;
-
- else low = mid+1;
-
- }
-
- if(low == 301)return -1;
-
- return low;
-
-
-
- }
-
-
-
- int main(){
-
- while(scanf("%d%d",&M,&N) != EOF){
-
- if(M == 0 && N == 0)break;
-
- nCnt = 0;
-
- FinalState = 0;
-
- for(int i = 0; i < M; ++i){
-
- scanf(" %s",map[i]);
-
- for(int j = 0; j < N; ++j){
-
- if(map[i][j] == 'F'){
-
- sid = nCnt;
-
- nodes[nCnt++] = node(i,j);
-
- FinalState += (1<<sid);
-
- }
-
- else if(map[i][j] == 'G')
-
- nodes[nCnt++] = node(i,j);
-
- else if(map[i][j] == 'Y'){
-
- int tid = nCnt;
-
- nodes[nCnt++] = node(i,j);
-
- FinalState += (1<<tid);
-
- }
-
- }
-
- }
-
- memset(dist,-1,sizeof(dist));
-
- for(int i = 0; i < nCnt; ++i)BFS(nodes[i]);
-
- int ans = solve();
-
- printf("%d\n",ans);
-
- }
-
- return 0;
-
- }