状态压缩DP(使用位运算加速)
这是个经典的状态压缩DP,为加深印象详细写写一下报告,由于是中文题目所以不说题意了
思考方法:首先,一个炮的攻击有两行,所以对于第i行来讲,i-1行和i-2行对它有影响,i-3行及以上的都没有影响了,所以我们要得到第i行的信息,只需要知道i-1和i-2的信息(最近有个体会,DP要找到什么因素影响了当前你要求的东西,有影响的我们就处理,没影响的我们不用管)。接着我们就思考怎么表示状态。山用1表示,空地用0表示,空地放了兵也用1表示,那么对于一行,就是一个01的串,这是个二进制数,我们可以想到状态压缩压缩回来一个十进制数。
比如原地图01101011,那么0处可以放兵,所有那么多个0,可以变为1(但也要考虑炮与炮之间不能攻击),要枚举全部情况,我们很自然想到了dfs来枚举,很多解题报告是这样做的,这样确实也能解决,但不是最好的方法。最好的方法是位运算。
要想到位运算,要跳出思维的限制,在一行中,原有的1是固定不能变,炮不能放在山上。0是可以变为1的,但是要保证炮与炮之间不能攻击。要满足这两个要求,我们可以拆开来做。先满足了炮与炮不能互相攻击,然后在这些摆放中再选出跑不在山上的。
只考虑炮的话,枚举量2^10-1,但实际上满足的不足60个,网上有人问过60是怎么计算的,实际上准确的做法我也不确定,但是能大概推出来。一列最多10位,最多其实只能放4个炮,然后接着看3个炮,2个炮,1个炮,0个炮的情况,就可以大致算出。
枚举方法是 for(i=0; i<(i<<col); i++) if( !(i&(i<<1)) & !(i&(i<<2)) ) i是合法状态 这个是要点,要理解
int state[MAXM]; 保存状态(十进制数),是仅仅满足了炮与炮不互相攻击,但是没有满足炮不在山上
对于一开始的地图,还没放炮,它本身已经表示了一个状态,所以也先压成一个状态,保存在一个数组中
再者,我们得到了一个状态state[i],我们怎么知道这个状态下放了多少炮啊?其实就是判断state[i]这个十进制数变为二进制数后有多少个1,这个要怎么统计呢?位运算!
并且把对应的士兵人数保存在 int soldier[MAXM]; //对应着,在state[i]状态下能放多少个士兵
int base[MAXR]; //第i行的原地图压缩成的一个状态
那么怎么判断炮不在山上呢? 只要state[i] & base[r] = 0 ,就表示state[i]这个状态,可以放在r这行上,而且炮不会在山上,炮之间也不会攻击,这是个要点,理解
然后前面说了,i行,i-1行,i-2行的炮会互相影响,他们可能会互相攻击到对方,所以我们假设现在i行,i-1行,i-2行的炮的摆放情况分别是state[i],state[j],state[k]
只有当他们都不两两攻击的时候,这3个状态才能放在一起,否则这3个状态不能放在一起。那么怎么判断他们不会两两攻击呢,方法一样的
state[i] & state[j] = 0 state[i] & state[k] = 0 state[j] & state[k] = 0 ,三个要同时满足,要点,理解
你会发现多次需要用到 一种判断 a&b 是为0还是不为0,所以我们代码中将其写成宏定义方便查看,实际上写成宏后时间慢了一点
#define legal(a,b) (a&b) //判断两个状态共存时是否合法,合法为0,不合法为非0
最后看状态转移方程,设dp[r][i]表示第r行,状态为state[i]是的最大值,
dp[r][i]=max { dp[r-1][j]+dp[r-2][k] } + soldier[i]
也就是第r-1行的状态为state[j],第r-2行的状态为state[k]的和,并加上当前行放了soldier[i]个士兵 , 但要满足state[i],state[j],state[k]不能互相攻击
接着我们可以可以写成三维的形式 dp[r][i][j]= max{ dp[r-1][j][k]} + soldier[i] , dp[r][i][j]表示第r行状态为state[i],第r-1行为state[j]的最大值
然后可以看代码了
#include <cstdio> #include <cstring> #define MAXR 110 //行数 #define MAXC 15 //列数 #define MAXM 70 //状态数 #define max(a,b) a>b?a:b //返回较大值 #define CL(a) memset(a,0,sizeof(a)) //初始化清空数组 #define legal(a,b) a&b //判断两个状态共存时是否合法,合法为0,不合法为非0 int row,col; //行列 int nums; //仅是两个炮兵不互相攻击的条件下,符合条件的状态个数 int base[MAXR]; //第i行的原地图压缩成的一个状态 int state[MAXM]; //仅是两个炮兵不互相攻击的条件下,符合条件的状态(一个十进制数) int soldier[MAXM]; //对应着,在state[i]状态下能放多少个士兵 int dp[MAXR][MAXM][MAXM]; //dp[i][j][k] 表示第i行状态为state[j],第i-1行状态为state[k]时的最优解 char g[MAXR][MAXC]; int main() { CL(base); CL(state); CL(soldier); CL(dp); nums=0; scanf("%d%d",&row,&col); for(int i=0; i<row; i++) //先计算原始地图的状态数 { scanf("%s",g[i]); for(int j=0; j<col; j++) if(g[i][j]=='H') base[i]+=1<<j; //像0110000,这里计算为6 } for(int i=0; i<(1<<col); i++) //仅是两个炮兵不互相攻击的条件下计算所有状态 { if( legal(i,i<<1) || legal(i,i<<2)) continue; //i这个状态出现了士兵两两攻击 int k=i; while(k) //这个循环计算状态i的二进制形式里面有多少个1,也就是放了多少个士兵 { soldier[nums]+=k&1; //等价于k%2,相当于判断k的二进制形式里面有多少个1 k=k>>1; } state[nums++]=i; //保存这个合法的状态 } /***************************************/ //for(int i=0; i<nums; i++) printf("%d %d\n",state[i],soldier[i]); /***************************************/ for(int i=0; i<nums; i++) //先初始化dp[0][i][0],即初始化第1行的情况 { if(legal(state[i],base[0])) continue; //在state[i]的基础上,还要满足士兵不能放在山上,这个判断就是处理这个问题的 dp[0][i][0]=soldier[i]; } for(int i=0; i<nums; i++) //接着初始化dp[1][i][j],即第2行的情况 { if(legal(state[i],base[1])) continue; for(int j=0; j<nums; j++) //枚举第1行的状态 { if(legal(state[j],base[0])) continue; if(legal(state[i],state[j])) continue; dp[1][i][j]=max(dp[1][i][j] , dp[0][j][0]+soldier[i]); //状态转移方程 } } for(int r=2; r<row; r++) //第3行开始DP直到最后 for(int i=0; i<nums; i++) //枚举第r行的状态 { if(legal(state[i],base[r])) continue; for(int j=0; j<nums; j++) //枚举第r-1行的状态 { if(legal(state[j],base[r-1])) continue; if(legal(state[i],state[j])) continue; //第r行的士兵和第r-1行的士兵相互攻击 for(int k=0; k<nums; k++) //枚举第r-2行的状态 { if(legal(state[k],base[r-2])) continue; if(legal(state[j],state[k])) continue; //第r-1行的士兵和第r-2行的士兵相互攻击 if(legal(state[i],state[k])) continue; //第r行的士兵和第r-1 dp[r][i][j]=max(dp[r][i][j] , dp[r-1][j][k]+soldier[i]); } } } int ans=0; for(int i=0; i<nums; i++) for(int j=0; j<nums; j++) //枚举dp[row-1][i][j] ans=max(ans,dp[row-1][i][j]); printf("%d\n",ans); return 0; }