状压dp(状态压缩,用一个数表示一组状态,降低状态表示所需维数)

这里写目录标题

    • 熄灯问题(状压+枚举) 用二进制数的大小来枚举一行开关的状态
    • ~~真~~ 状压入门题,铺棋盘(状压dp)
    • 牧场种草方法总数(草地不相邻,只能种在肥沃土地上)
      • 思路:
      • ==1、用了位运算就给我使劲打括号==
      • 2、判断状态是否合法
      • ==bitset< 1<<12 >legal ;==
      • 3、灵活使用位运算
      • 4、状压后状态范围 0 ~ (1<< n)-1
      • 5、状态从0开始遍历,行啊列啊,从1开始列举

熄灯问题(状压+枚举) 用二进制数的大小来枚举一行开关的状态

#include 
#include 
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
using namespace std;
char orilights[5] ;
char lights[5] ;
char result[5] ;
int getBit(char x,int i){//获取x的第i个比特 (从低位到高位),x为char类型,刚好8比特 
	return (x>>i)&1; 
}
void setBit(char& x,int i,int v){//设置x的第i个比特设置成v,要改变x的实参 
	 if(v==1) x=x|(1<<i);//1|1==1,1|0=1
	 else x&=~(1<<i) ;
}
void FlipBit(char& x,int i){
	x^=(1<<i);//0/1与1求异或就是取反 
}
int main(int argc, char** argv) {
	//step1 先存起来灯的初始状态
	int s;
	for(int i=0;i<5;i++){
		for(int j=0;j<6;j++){
			cin>>s;
			setBit(orilights[i],j,s);
		}
	} 
	//5行6列 ,每一行的开关选取状态对应一个整数 
	//要输出的result开关选取数组result[5]   原来灯的状态数组 orilights[5] 
	int switchs;
//	for(int i=0;i<5;i++){//考虑每一行 
//}
		for(int n=0;n<64;n++){//第一行的开关选取组合 
			memcpy(lights,orilights,sizeof(lights));
				switchs=n;
				for(int j=0;j<5;j++){//假设第j行的开关状态已知 
					result[j]=switchs;//选好了第0行开关组合 改变原来灯的状态 
			for(int i=0;i<6;i++){//第i行的开关一个个看过去 
				if(getBit(switchs,i)==1){//开关组合选择中按下的是1才发生变化 
					if(i>=1) FlipBit(lights[j],i-1);
					if(i<=4) FlipBit(lights[j],i+1);
					FlipBit(lights[j],i);
				}//同一行的灯的处理,左边 右边 自己 
			}
//第j行的开关不仅影响第j行,还影响第j+1行,
//对j-1的影响就是把他全部熄灭 ,就是以此为条件决定第j行开关情况的 
			if(j<5)lights[j+1]^=switchs;//首先可以确定第1行的状态,再次能确定第1行的开关选择 
			switchs=lights[j];
				}
				if(lights[4]==0){
					for(int i=0;i<5;i++){
						for(int j=0;j<6;j++){
							printf("%d ",getBit(result[i],j));
						}
						printf("\n");
					}
					break;
				}
		}

	return 0;
		
}

状压入门题,铺棋盘(状压dp)

Mondriaan’s Dream

有一个NM(N<=5,M<=1000)的棋盘,现在有12及2*1的小木块无数个,要盖满整个棋盘,有多少种方式?答案只需要mod1,000,000,007即可。

int dp[10005][40];
//状压换的是状态的列举方式,不变的是递推思想,
表示第i列的为state状态的 前i-1列铺木块方式
因为 第i列的state状态 是由前i-1的状态 推过来的
对 第i列(state状态下,考虑第i-1列的木块对它的影响)的状态进行 列举
所有可行的状态都会对应产生一个next状态(i+1列状态列举的预状态 )
每一个next状态得到的方法数就是dp[i+1][nex]=dp[i][state]
当然了前i列的状态不同组合下(第i列状态在这些组合下不一样),也有可能
得到相同的第i+1列的预状态,
所以得到的dp[i+1][nex]=dp[i+1][nex]+ dp[i][state]
至于对第i列在预状态下进行状态列举,就得从从1行到j行
(0,1的组合序列类似dfs求全排列),预状态决定了附近能否放牧快
类似于vis数组。对第j个位置能决定(到达)j+1,j+2个位置,再从j+1,j+2位置开始向下dfs,直到j==n,这一列就状态就结束了,一种状态(01组合)列举完了(一条路走完了),就可以为它推出的下一列nex状态的方法数
dp[i+1][nex] 加上它的贡献值

#include 
#include 
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
using namespace std;
typedef long long ll;//全换ll,一了百了 
long long int dp[2007][2007];//再空间允许范围内开大 
//后面是状态数,不能根据 5行为一列状态数开40,又输入6行(64种) 8列 
const int mod=1000000007;
int n,m;//n<=5,m<=1000,至多5行,至多1000列 
//状压换的是状态的列举方式,不变的是递推思想,
void  dfs(ll i,ll j,ll state,ll nex){
	if(j==n){
		dp[i+1][nex]=dp[i+1][nex]+dp[i][state];
		//之前前i列的某种组合(其中第i列不是state状态,也能得到第i+1列nex这种状态 
		dp[i+1][nex]%=mod;
		return ;
	}
	if((state&(1<<j))>0)dfs(i,j+1,state,nex); 
	if((state&(1<<j))==0){//第j行是否放了木块,
//注意了将一个数右数第j位设置为1,k&1<<(j-1)),可这里j是下标对应j+1位 
		dfs(i,j+1,state,nex|(1<<j));
		//横着放一块,产生一种i+1行预状态,下次从j+1行出发啦 
	}
	if((j+1<n)&&(state&(1<<j))==0&&(state&(1<<(j+1)))==0){
		dfs(i,j+2,state,nex);//竖着放一块 
	}
	return; 
} //遇到位运算,尽可能多地打括号 ,别用!判断是否非零 
//(state&(1<
int main(int argc, char** argv) {
//	int n,m;//n<=5,m<=1000,至多5行,至多1000列 
//一列一列的铺,因为一列普下来的状态较小,可以状压用二进制表示
//因此循环外层遍历的是列 
cin>>n>>m;
memset(dp,0,sizeof(dp));
dp[1][0]=1;//第一列0状态就一种01组合 
	for(int i=1;i<=m;i++){//列 
		for(int s=0;s<(1<<n);s++){
			if(dp[i][s]){//前i-1列得到的第i列预状态的方法数 
				dfs(i,0,s,0);//列从i列举(1~m),行从0列举0~n-1 
			} 
		}
	} 
	cout<<dp[m+1][0];//前m列摆满了,对第m+1列的预状态就是0 
	return 0;
		
}

牧场种草方法总数(草地不相邻,只能种在肥沃土地上)

题目链接

思路:

和在棋盘上铺木块很像,要求解的方法数等于到达最终状态的总方法数,用dp数组来累加到达某行某状态的方法数,一旦前面i-1行的若干组合中出现这种到达i行的state状态,就加到dp【i】【state】
if( dp【i前】【state前】)表示的就是之前i-1行种达到state前这种状态的方法数,只有这种组合存在(至少有一种方法能达到这种状态),才能由这种状态推出第i行的各种state状态

#include 
#include 
#include
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
using namespace std;
typedef long long ll;//全换ll,一了百了 
//int a[15][15]; 
ll init[15]; 
ll legal[1<<12];//记录legal[s]是否为1 
//bitset<1<<12>legal;
ll m,n;
ll dp[15][1<<12];//前i行,第i行为j状态时的最大方法数 
const ll mod=100000000;
int main(int argc, char** argv) {
	cin>>m>>n;
	int d;
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			cin>>d;
			init[i]=(init[i]<<1)+d;//一行压缩成一个二进制数init[i] 
		}
	}
	for(int s=0;s<(1<<n);s++){
//首先判断该状态相邻不种草,到具体某一行再去判断状态与草地肥沃是否吻合 
//先用legal数组索引合法状态,不用每次都去判断 
//		if((s&(s<<1)==0)&&(s&(s>>1)==0))legal[s]=1; 
		if(((s&(s<<1))==0)&&((s&(s>>1))==0))legal[s]=1; 
	}
	dp[0][0]=1;
	for(int i=1;i<=m;i++){
		for(int s=0;s<(1<<n);s++){
//种草的总方法数就是最后一行达到的所有合法状态的数量之和
//达到//先判断这种状态是否可能存在,一是能否种草,二是种草的左右不能有草
//一种状态,就加在dp[i][state](所有到第i行为止达到state的方法数总合)上 
		if(legal[s]&&((s&init[i])==s)){//合法匹配是1 1,0 1,0 0
			for(int sl=0;sl<(1<<n);sl++)//枚举上一行状态 
			if(dp[i-1][sl]&&((s&sl)==0))dp[i][s]=(dp[i][s]+dp[i-1][sl])%mod; 
//			dp[i][s]=(dp[i][s]+dp[i-1][all])%mod;
		} 
		}
	} 
	ll cnt=0; 
	for(int s=0;s<(1<<n);s++){//最后一行以所有状态结束的总方法 
		if(dp[m][s])cnt=(cnt+dp[m][s])%mod;
	}
	cout<<cnt;
	return 0;
}

1、用了位运算就给我使劲打括号

if((s&(s<<1)==0)&&(s&(s>>1)==0))legal[s]=1;if(((s&(s<<1))==0)&&((s&(s>>1))==0))legal[s]=1;

2、判断状态是否合法

先判断这种状态是否可能存在,一是能否种草,二是种草的左右不能有草

int getBit(int s,int j){//1110101
	return s&(1<<(j-1); 
}
bool legal(int i,int s){//在肥沃之地种草,不能出现左右相邻的情况 
	for(int j=1;j<=n;j++){
			if(getBit(s,j)>a[i][n-j]){//从右数第j个数下标是n-j 
				return false; 
			}
	}
//可以不需要a数组,将初始草地肥沃与否的状态也压缩成一个二进制数,
//与s相与不为0 
	if(s&(s<<1)!=0||s&(s>>1)!=0)return false; 
}
到了某一行的某个状态,就去判断,这样对状态是否合法的判断重复了很多
不同的行会重复判断同样的状态是否合法 

状态范围已知,不如先用数组记录合法的状态,到了具体某一行再具体和这一行的init[i]状态比较

记录合法的状态,可以用判断数组 legal【state]=1
也可以记录在数组种,legal【len】=state
还有一种容器 bitset
bitset用法

bitset大概就是类似于bool数组一样的东西

但是它的每个位置只占1bit(特别特别小)

bitset的原理大概是将很多数压成一个,从而节省空间和时间(暴力出奇迹)

一般来说bitset会让你的算法复杂度 /32(具体是什么要看计算机)
使用bitset类型需#include < bitset >

bitset类型在定义时就需要指定所占的空间,例如

bitset< 1<<12 >legal ;

3、灵活使用位运算

1)、判断两个1不能相邻
s&(s<<1)==0&&s&(s>>1)==0
!!!你看看,上面又没打括号
(s&(s<<1))==0&&(s&(s>>1))==0

2)、init[x] 0 1 1
state 0 1 0
满足这种对应就是init[x]&state == state
3)、两个数中1的位置不能相同
x1&x2==0

4、状压后状态范围 0 ~ (1<< n)-1

n个二进制数的状态状压之后范围是 0 ~ (1<

所以列举状态时
for(int s=0;s<(1< 如果像这样每行状态范围相等,可以用maxs来避免这许多次位运算

5、状态从0开始遍历,行啊列啊,从1开始列举

状态从0开始遍历,行啊列啊,从1开始列举比较好,因为一开始要初始化, 第1行才有意义

你可能感兴趣的:(状压dp,动态规划,cpp)