壮压DP 经典例题:Corn Fields 炮兵阵地

状态压缩动态规划,就是我们俗称的状压DP,是利用计算机二进制的性质来描述状态的一种DP方式
状压,顾名思义就是要将一些状压想办法压缩起来(可以压,也可以删)。其中这些状态都满足相似性和数量很多。这样才好压而且压得有意义。常见于一般的方格题,网络题等等

## 一般基础的状压就是将一行的状态压成一个数,这个数的二进制形式反映了这一行的情况 所以位运算可以帮助我们解决很多问题
1.& 符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
运算规则为 " 0&0=0、0&1=0、1&0=0、1&1=1 " (理解为取交集)
2.| 符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
运算规则为 " 0|0=0、0|1=1、1|0=1、1|1=1 "(理解为取并集)
3.^ 符号,x ^ y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
运算规则为 "0 ^ 0=0、0 ^ 1=1、1 ^ 0=1、1 ^ 1=0 "(相同为0,不同为1)
4.<< 符号是左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。
5.>> 符号是右移操作,x>>1表示去掉x二进制下的最右一位。x>>1相当于给x/2。
6.~ 符号是按位取反,规律:~x= -(x+1)。

这四种运算在状压dp中有着广泛的应用,常见的应用如下:

1.判断一个数字x二进制下第i位是不是等于1。

方法:if(((1<<(i−1))&x)>0)

将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。

2.将一个数字x二进制下第i位更改成1。

方法:x=x|(1<<(i−1))

证明方法与1类似,此处不再重复证明。

3.把一个数字二进制下最靠右的第一个1去掉。

方法:x=x&(x−1)
4.判断二进制数是否有两个相邻的1

方法:if(x&(x<<1)==0) 说明没有 不满足说明有

5.判断二进制数是否在距离1两个单位内有1存在

方法:if(x&(x<<1)==0 && x&(x<<2)==0) 说明没有 不满足说明有
在这里插入图片描述
例题:Corn Fields
题目大意:农夫有一块地,被划分为m行n列大小相等的格子,其中一些格子是可以放牧的(用1标记),农夫可以在这些格子里放牛,其他格子则不能放牛(用0标记),并且要求不可以使相邻格子都有牛。现在输入数据给出这块地的大小及可否放牧的情况,求该农夫有多少种放牧方案可以选择(注意:任何格子都不放也是一种选择,不要忘记考虑!

代码注释如下:

#include <cstdio>
#include <cstring>
using namespace std;
const int mod=100000000;
int M,N,top = 0;//top表示每行最多的状态数,即如果state中所有状态可行的情况都能种植的话(不是坏土地),则每行状态数就为top 
int state[600],num[110];  //state存放每行所有的可行状态(即没有相邻的状态) 
int dp[20][600];//dp[i][j]:对于前i行数据,当第i行采用第j种状态时的方案数 
int cur[20];//cur[i]表示的是第i行整行的土地情况(但是以相反方式存取的,这样便于与state数组中的状态进行与比较,更方便的判断state中哪些状态是可行的),即哪些可种植,哪些不可种植,二进制表示状态的十进制数 

bool ok(int x)//判断状态x是否可行
{	
   if((x&(x<<1))==0) return true;//若存在相邻两个格子都为1,则该状态不可行  对于二进制数x,只有当x&(x<<1)==0时,二进制数x不会有相邻两个1 
   return false;
}

void init()	//遍历所有可能的状态     /假设两行三列。则遍历 000 001 010 011 100 101 110 111,满足没有相邻1的000 001 010 100 101存进数组state/
{		
   top = 0;
   int total = 1 << N; //一行的最大状态数 
   for(int i = 0; i < total; ++i)
   {
       if(ok(i))//如果此状态可行就存进去state数组 
	   	state[++top] = i;	
   }
}

bool fit(int x,int k)//判断状态x 与第k行的实际状态的逆是否有‘重合’ 
{ 
   if((x&cur[k])==0) return true; //若没有重合,(则x符合要求) 位运算注意符号优先级,带括号!! 
   return false;  //若有重合,则不可行
}
 
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) //输入时就要按位来存储,cur[i]表示的是第i行整行的土地情况,每次改变该数字的二进制表示的一位
		   {  
                scanf("%d",&num);  //表示第i行第j列的情况(0或1)
               	if(num == 0) //若该格为0	
				   cur[i] +=(1<<(N-j)); //则将该位置为1(注意要以相反方式存储,即1表示不可放牧    即若输入第一行是111 存进去的cur[1]二进制数为000,便于下面的在state数组中筛选 
           }
       }
       for(int i = 1;i <= top;i++)
	   {
           if(fit(state[i],1))//判断所有可能状态与第一行的 "实际状态的逆" 是否有重合,即初始化 
		   {  
                dp[1][i] = 1; //若输入的第1行的状态与第i种可行状态吻合,则dp[1][i]记为1
           }
	   }
	   /*
	   状态转移过程中,dp[i][k] =Sigma dp[i-1][j] (j为符合条件的所有状态)
		*/
       for(int i = 2; i <= M; ++i)//i索引第2行到第M行
	   {  
           for(int k = 1; k <= top; ++k)//该循环针对所有可能的状态,找出一组与第i行相符的state[k]
		   { 
                if(!fit(state[k],i))//判断是否符合第i行实际情况,不符合的话直接下一循环,每次找到一个符合的再往下判断 
					continue; 
                for(int j = 1; j <= top ;++j)//找到state[k]后,再找一组与第i-1行符合,且与第i行(state[])不冲突的状态state[j]
				{ 
                   if(!fit(state[j],i-1))//判断是否符合第i-1行实际情况
				   		continue;  
                   if(state[k]&state[j]) //判断是否与第i行冲突
				   		continue; 
                   dp[i][k] = (dp[i][k] +dp[i-1][j])%mod; //若以上皆可通过,则将'j'累加到‘k'上    /即对于前i行,第i行选用第j状态时得到的方案数 
                }
           }
       }
       int ans = 0;
       for(int i = 1; i <= top; ++i)
	   {
           ans = (ans + dp[M][i])%mod; //累加最后一行所有可能状态的值,即得最终结果!!!泥马写注释累死我了终于写完了!
       }
       printf("%d\n",ans);
   }
}
/*总思路是:1.先进行init函数与ok函数   找出没有(有些土地不能种植)这一条件时,对一行中所有满足条件(相邻土地不能一块种植)的状态,用二进制数表示,十进制数存进state数组 
			2.用一个数组cur存入每一行给出的土地能否种植的状态,但存储方式为相反存储,如若某一行为1 1 1,代表均可放牧,则存进去cur时二进制数为0 0 0
			在cur二进制数表示的状态中1表示不可放牧,0表示可放牧 
			3.然后需要初始化第一行,通过fit函数,就可以判断出state数组中的哪些状态是符合第一行土地要求的,符合要求的i状态dp[1][i]=1; 
			4.初始化完第一行后,开始从第二行开始遍历直到第M行,每次在state数组中找到一个满足本行土地要求的一个状态,然后再在state中找到一个满足上一行土地要求的一个状态
			如果两个状态不冲突的话(位与=0),那么就说明此行的状态可取,更新dp[i][k]+=dp[i-1][j]   其中k为当前行的一个可行状态,j为上一行的一个可行状态 
			5.遍历完之后,答案就为 Sigma dp[M][i]  i为第M行符合条件的所有状态 
*/ 

例题:炮兵阵地
司令部的将军们打算在NM的网格地图上部署他们的炮兵部队。一个NM的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

Input
第一行包含两个由空格分割开的正整数,分别表示N和M;
接下来的N行,每一行含有连续的M个字符(‘P’或者’H’),中间没有空格。按顺序表示地图中每一行的数据。N <= 100;M <= 10。

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

Output仅一行,包含一个整数K,表示最多能摆放的炮兵部队的数量。

6

代码注释如下:

#include<cstdio>
#include<cstring>
#include<string>
#include<iostream>
using namespace std;
 
int state[105],stn[105];//state数组用来存储一行中所有可行的状态,stn数组用来存储每种状态所包含的炮兵数 
long long int dp[105][105][105];//dp[i][j][k]表示对于前i行,当第i行选用j状态,第i-1行选用k状态时最多可部署的炮兵数 
int n,m,rem[105];//rem数组用来记录每一行的地图状态,为反向存储,即不可放置的记为1,可放置的记为0   在state数组中是可放置的记为1,不可放置的记为0,反向存储的目的是为了在state数组中筛选满足地形时的状态时运用位与运算方便筛选 
int most,total;
 
void find_all_state()//找到一行中不考虑地势的情况下所有满足的状态数 
{
	most=0;
	total=1<<m;//一行中的最大状态数 
	for(int i=0;i<total;++i)
	{
		if((i&(i<<1))==0&&(i&(i<<2))==0)//满足此条件说明此状态满足与1距离2以内不会有1存在 
		{
			state[++most]=i;//存取十进制数 
			stn[most]=0;
			for(int j=1;j<=state[most];j=(j<<1))
			{
				if(j&state[most])//判断state[most]的二进制数有多少个1 
					++stn[most]; //计算出该状态炮兵的个数 
			}
		}
	}
}
 
inline bool suit(int x,int y)//判断状态x 与第y行的实际状态的逆是否有‘重合’
{
	if((x&y)==0) return true;//若没有,返回真 
	return false;//否则返回假 
}
 
int main()
{
	while(scanf("%d%d",&n,&m)!=EOF)
	{
		memset(dp,0,sizeof(dp));
		memset(state,0,sizeof(state));
		memset(stn,0,sizeof(stn));
		memset(rem,0,sizeof(rem));
		find_all_state();
 
		for(int i=1;i<=n;++i)
		{
			string s;
			cin>>s;
			for(int j=0;j<s.size();++j)
			{
				if(s[j]=='H')//山地,不可放置的	
					rem[i]+=(1<<(m-j-1));//反向存储,标记不可放置的位置为1
			}
		}
		int ans=-1;//最终结果
		for(int i=1;i<=most;++i) 
		{
			if(suit(rem[1],state[i])) //判断所有可能状态与第一行的 "实际状态的逆" 是否有重合,即初始化,如果没有重合则此状态可取 
			{
				dp[1][i][1]=stn[i];//所有满足第一行的状态都赋了值 
				if(n==1)	
					ans=ans>dp[1][i][1]?ans:dp[1][i][1];//若只有一行,那么就可直接得出最大炮兵数 
			}
		}
		if(n>=2) 
		{
			for(int i=1;i<=most;++i)
			{
				if(suit(rem[2],state[i])) //在所有可能状态中找到满足第二行的状态进行遍历 
				{
					for(int j=1;j<=most;++j) //再找到一个状态给第一行,满足两个状态不重合的情况下进行比较 ,你不需要疑问此状态是否跟第一行实际状态的逆有重合,有重合的话dp就为0,我们选择的是较大的,就不会去选择此情况 
					{
						if(suit(state[i],state[j]))
						{
							dp[2][i][j]=dp[2][i][j]>dp[1][j][1]+stn[i]?dp[2][i][j]:dp[1][j][1]+stn[i];//更新 
							if(n==2)	
								ans=ans>dp[2][i][j]?ans:dp[2][i][j];//若只有两行,则在此可计算出最大炮兵数 
						}
					}
				}
			}
		}
		for(int i=3;i<=n;++i)//搞定了前两行,然后从第三行开始遍历 
		{
			for(int j=1;j<=most;++j)//当第i行取第j种状态
			{
				if(!suit(state[j],rem[i]))	//若j与实际情况冲突
					continue;
				for(int k=1;k<=most;++k) //第i-1行取第k种状态
				{
					if(!suit(state[j],state[k])) //若k与j冲突
						continue;
					for(int l=1;l<=most;++l)//第i-2行取第l种状态
					{
						if(!suit(state[j],state[l]))//l不能与j冲突	
							continue;
						if(!suit(state[k],state[l]))//l不能与k冲突
							continue;
							
						dp[i][j][k]=dp[i][j][k]>dp[i-1][k][l]+stn[j]?dp[i][j][k]:dp[i-1][k][l]+stn[j];//满足不存在冲突的情况下更新 
 
						if(i==n)	
							ans=ans>dp[i][j][k]?ans:dp[i][j][k];
					}
				}
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

细心的同学可以发现,这两题有很大的类似之处,这正是这类单考壮压DP的题的类型,有那么一点点的类似板子题,先是存储所有每一行的可行状态到数组state中,然后再根据反向存储输入的棋盘结构筛选state中的状态。在存进去state数组中时要思考出二进制的计算方式,哪一种状态是我们需要的,这是关键点。然后再初始化第一行,或前两行。然后再往下依次遍历,每次在一行给出一个状态,满足状态不冲突的情况下更新值。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

henulmh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值