动态规划本来就很抽象,状态的设定和状态的转移都不好把握,而状态压缩的动态规划解决的就是那种状态很多,不容易用一般的方法表示的动态规划问题,这个就更加的难于把握了。难点在于以下几个方面:状态怎么压缩?压缩后怎么表示?怎么转移?是否具有最优子结构?是否满足后效性?涉及到一些位运算的操作,虽然比较抽象,但本质还是动态规划。找准动态规划几个方面的问题,深刻理解动态规划的原理,开动脑筋思考问题。这才是掌握动态规划的关键。
动态规划最关键的要处理的问题就是位运算的操作,容易出错,状态的设计也直接决定了程序的效率,或者代码长短。状态转移方程一定要仔细推敲,不可一带而过,要思考为什么这么做,掌握一个套路,遇见这类问题能快速的识别出问题的本质,找出状态转移方程和DP的边界条件。
下面是一些关于状态压缩DP的题目,大都不难。状压DP的东西还有很多,还会接着总结。
【题目大意】一个矩阵里有很多格子,每个格子有两种状态,可以放牧和不可以放牧,可以放牧用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>
- usingnamespacestd;
- #definemod100000000
- intM,N,top=0;
- intstate[600],num[110];
- intdp[20][600];
- intcur[20];
- inlineboolok(intx){
- if(x&x<<1)return0;
- return1;
- }
- voidinit(){
- top=0;
- inttotal=1<<N;
- for(inti=0;i<total;++i){
- if(ok(i))state[++top]=i;
- }
- }
- inlineboolfit(intx,intk){
- if(x&cur[k])return0;
- return1;
- }
- inlineintjcount(intx)
- {
- intcnt=0;
- while(x)
- {
- cnt++;
- x&=(x-1);
- }
- returncnt;
- }
- intmain(){
- while(scanf("%d%d",&M,&N)!=EOF){
- init();
- memset(dp,sizeof(dp));
- for(inti=1;i<=M;++i){
- cur[i]=0;
- intnum;
- for(intj=1;j<=N;++j){
- scanf("%d",&num);
- if(num==0)cur[i]+=(1<<(N-j));
- }
- }
- for(inti=1;i<=top;i++){
- if(fit(state[i],1)){
- dp[1][i]=1;
- }
- }
- for(inti=2;i<=M;++i){
- for(intk=1;k<=top;++k){
- if(!fit(state[k],i))continue;
- for(intj=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;
- }
- }
- }
- intans=0;
- for(inti=1;i<=top;++i){
- ans=(ans+dp[M][i])%mod;
- }
- printf("%d\n",ans);
- }
- }
#include <cstdio> #include <cstring> using namespace std; #define mod 100000000 int M,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",&N)!= EOF){ init(); memset(dp,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); } }
【题目大意】类似于上面一道题,一个方格组成的矩阵,每个方格可以放大炮用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>
- usingnamespacestd;
- #definemax(a,b)(a)>(b)?(a):(b)
- intN,M;
- charmap[110][20],num[110],top;
- intstk[70],cur[110];
- intdp[110][70][70];
- inlineboolok(intx){//判断该状态是否合法,即不存在相邻的1之间的距离小于3的
- if(x&(x<<1))return0;
- if(x&(x<<2))return0;
- return1;
- }
- //找到所有可能的合法状态,最多60种
- inlinevoidjinit()
- {
- top=0;
- inti,total=1<<N;
- for(i=0;i<total;i++)if(ok(i))stk[++top]=i;
- }
- //判断状态x是否与第k行匹配
- inlineboolfit(intx,intk)
- {
- if(cur[k]&x)return0;
- return1;
- }
- //数一个整型数x的二进制中1的个数(用于初始化)
- inlineintjcount(intx)
- {
- intcnt=0;
- while(x)
- {
- cnt++;
- x&=(x-1);
- }
- returncnt;
- }
- intmain(){
- while(scanf("%d%d",&N)!=EOF){
- if(N==0&&M==0)break;
- jinit();
- for(inti=1;i<=M;++i)scanf("%s",map[i]+1);
- for(inti=1;i<=M;++i)
- for(intj=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(inti=1;i<=top;i++){
- num[i]=jcount(stk[i]);
- if(fit(stk[i],1))
- dp[1][1][i]=num[i];
- }
- inti,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]);
- }
- }
- }
- }
- intans=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);
- }
- return0;
- }