动态规划之状态压缩(从入门到入土)

状态压缩前言

  • 对于 n < = 20 n<=20 n<=20 的数据范围,不妨考虑一下记忆化搜索和状态压缩

  • 主要思想: 二进制位运算

  • 举个例子: 一条街上有n个路灯,路灯只有亮与不亮两种状态。如果去描述这n个路灯的状态呢???用数组描述显然可以,但有没有其他的方法???

  • 二进制下的一个整数 k !!!

  • 例如:n 等于 8,k = 1011011 1 ( 2 ) 10110111_{(2)} 10110111(2)

  • 其他功能怎么办??? 比如判第i个灯是否亮,关闭第i个灯,如何关闭多个灯之类的

  • 位运算来了!!!

int main(){
	if(k&(1<<i));//判断第i位是否为1 
	k=k|(1<<i);//将第i位为变成1 
	k=k&(!(1<<i));//将第i位为变成0 
	if(i&j);//状态i与状态j有重复
	if((i<<1)&j);//状态i与状态j存在右上左下同时1
	if(i&(j<<1));//状态i与状态j存在左上右下同时1
	if(j&(j<<1));//状态j自身存在连续1
	if(j&(~a[i]));//状态j在不该存在1的地方存在了1 
}

状态压缩实现记忆化搜索

套路

  • 对于选与不选问题,我们有暴力 O ( 2 n ) O(2^n) O(2n) 的算法。
  • 对于第 i 次选哪个问题,我们有暴力 O ( n n ) O(n^n) O(nn) 或者 O ( n ! ) O(n!) O(n!) 的算法。
  • 记忆化搜索: 要是我们能记录下已经选择了若干个的状态的最优值,在我们遇到重复状态时,我们就可以直接返回记忆化的值了。数组的下标只能是整数,我们可以用二进制去存储选取状态,这样选择若干个的状态就可以用一个整数代替了。

例题1:单词接龙

例题链接

  • 题目描述: 有 n 个字符串,对其做单次接龙(当前单词尾字母与下一个单词的首字母相同),求连接成的长度最长为多少。第一个单词可以任意选,要求每个单词只能用一次。n<=18
  • 状态表示: f [ s t a t e ] [ l a s t ] f[state][last] f[state][last]:选取状态为state,且最后一个单词为第 last 个的后续 dfs 可以达到的最长长度(不包括第 last 个及以前的)。 然后记忆化搜索即可。
#include<bits/stdc++.h>
using namespace std;

string s[100];
int n,f[1000010][20];

int dfs(int state,int last){ 
    if(f[state][last])return f[state][last];
    int ans=0;
	for(int i=0;i<n;i++){
		if((1<<i)&state)continue;
		if(s[i][0]==s[last][s[last].size()-1])ans=max(ans,dfs((1<<i)|state,i));
	}
	return f[state][last]=ans+s[last].size();
}
int main() {
    cin>>n;
    for(int i=0;i<n;i++)cin>>s[i];
    int ans=0;
    for(int i=0;i<n;i++)ans=max(ans,dfs(1<<i,i));
	cout<<ans;
	return 0;
}

集合问题

套路

例题1:最小分组

例题链接

  • 题目描述: n个物品,体积分别为 w[i],现把其分成若干组,要求每组总体积<=W,问最小分组。n<=18
  • 问题分析: n 这么小,考虑状态压缩。为了使与二进制挂钩,自然将 n 个物品变为选与不选的情况。不妨尝试以下状态表示。
  • 状态表示: f [ j ] f[j] f[j]:选取情况为状态 j 时,需要多少个分组。 s i z e [ j ] size[j] size[j]:选取情况为状态 j 时,
  • 转移方程: f [ i ] [ j ] = f[i][j]= f[i][j]=

棋盘放置问题(记方案数)

套路

  • 状态表示: 通常设 f [ i ] [ j ] [ ] f[i][j][] f[i][j][] 为摆放完了前 i 行,第 i 行状态为 j ,前 i 行一共满足了多少要求下的方案数。
  • 预处理: 该类型的 i 通常从 2 开始, f [ 1 ] f[1] f[1] 通常需要通过预处理来得出
  • 限制: 棋盘放置问题通常有棋子不可相邻放置等限制,这时候我们需要通过一些位运算,来将不合法的状态转移给去掉。

例题1:棋盘放置国王

例题链接

  • 题目描述: N × N N×N N×N 的棋盘里面放 K K K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。 n < = 9 n<=9 n<=9
  • 状态表示: f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]:从第 1 行到第 i 行,第 i 行放置情况为 j 状态下,一共放了 k 个国王的方案总数。
  • 转移方程: f [ i ] [ j ] [ k ] + = f [ i − 1 ] [ p ] [ k − k i n g [ j ] ] f[i][j][k]+=f[i-1][p][k-king[j]] f[i][j][k]+=f[i1][p][kking[j]]
#include<bits/stdc++.h>
using namespace std;
long long f[12][1000][100],king[1000],n,k; 
int main() {
    //预处理下每个状态相当于放置了多少个国王 
	for(int j=0; j<(1<<n); j++) {
		int t=j;
		while(t) {
			king[j]+=t%2;
			t=t/2;
		}
	}
	//预处理初始值
	for(int j=0;j<(1<<n);j++){
		if(j&(j<<1))continue;
		f[1][j][king[j]]=1;
	}
    //状态转移 
	for(int i=2; i<=n; i++) {
		for(int j=0; j<(1<<n); j++) {
			if(j&(j<<1))continue;
			for(int p=0; p<(1<<n); p++) {
				if(j&p)continue;
				if(j&(p<<1))continue;
				if(j&(p>>1))continue;
				for(int k=K; k>=king[j]; k--) {
					f[i][j][k]+=f[i-1][p][k-king[j]];
				}
			}
		}
	}
	long long ans=0;
	for(int j=0;j<(1<<n);j++)ans+=f[n][j][K];
	cout<<ans;
}

例题2:牧场种草

例题链接

  • 题目描述: 要求在 n × m n\times m n×m 的牧场种草,草不能相邻放置。同时有些位置不能种草。请问一共有多少种选择草的方式 。n,m<=15
  • 状态表示: f [ i ] [ j ] f[i][j] f[i][j]:从第 1 行到第 i 行,第 i 行放置情况为 j 状态下,一共有多少种方案。
  • 转移方程: f [ i ] [ j ] + = f [ i − 1 ] [ p ] f[i][j]+=f[i-1][p] f[i][j]+=f[i1][p]
#include<bits/stdc++.h>
using namespace std;

long long f[15][5000],a[15],mod=100000000,n,m,x; 
int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cin>>x;
			a[i]=a[i]*2+x;
		}
	} 	
	//预处理初始值 
	for(int j=0;j<(1<<m);j++){
		if(j&(j<<1))continue;
		if(j&(~a[1]))continue;
		f[1][j]=1;
	}
	//状态转移 
	for(int i=2;i<=n;i++){
		for(int j=0;j<(1<<m);j++){
			if(j&(j<<1))continue; 
			if(j&(~a[i]))continue;
			for(int p=0;p<(1<<m);p++){
				if(p&(p<<1))continue;
				if(j&p)continue;
				if(p&(~a[i-1]))continue;
				f[i][j]=(f[i][j]+f[i-1][p])%mod;
			}
		}
	}
	long long ans=0;
	for(int j=0;j<(1<<m);j++)ans=(ans+f[n][j])%mod;
	cout<<ans;
}

例题3:地图放炮

例题链接

  • 题目描述: n × m n\times m n×m 的地图上放置炮,炮的攻击范围为上下左右距离 1 和 2 的点,有若干点不能放炮。要求在炮与炮不会互相攻击的情况下,最多可以放置多少个炮。 n < = 100 , m < = 10 n<=100,m<=10 n<=100,m<=10
  • 问题分析: 很显然,每层的放置不仅与上层有关还与上上层有关,因此我们还需多维护一维,即 令 j 位第 i 层的放置情况,令 p 位第 i-1 层的放置状态,令 q 位第 i-2 层的放置状态。
  • 状态表示: f [ i ] [ j ] [ p ] f[i][j][p] f[i][j][p]:从第 1 行到第 i 行,第 i 行放置情况为状态 j ,第 i-1 行放置情况为状态 p 下,一共有多少种方案。
  • 转移方程: f [ i ] [ j ] [ p ] = m a x ( f [ i ] [ j ] [ p ] , f [ i − 1 ] [ p ] [ q ] + k i n g [ j ] ) f[i][j][p]=max(f[i][j][p],f[i-1][p][q]+king[j]) f[i][j][p]=max(f[i][j][p],f[i1][p][q]+king[j])
  • 初始值: 要预处理两层了,即第 1 层状态为 j,第二层状态为 p 下的最多放炮数量
  • MLE:超内存了,滚动数组优化一下
#include<bits/stdc++.h>
using namespace std;

long long a[105],f[3][1025][1025],king[1025];
int main() {
	int n,m;
	char c;
	cin>>n>>m;
	for(int i=1; i<=n; i++) {
		for(int j=1; j<=m; j++) {
			cin>>c;
			if(c=='P')a[i]=a[i]*2+1;
			else a[i]=a[i]*2;
		}
	}
	//预处理每个状态下的放置的炮的数量
	for(int j=0; j<(1<<m); j++) {
		int t=j;
		while(t) {
			king[j]+=t%2;
			t=t/2;
		}
	}
	//预处理初始值
	for(int j=0; j<(1<<m); j++) {
		if(j&(j<<1))continue;
		if(j&(j<<2))continue;
		if(j&(~a[2]))continue;
		for(int p=0; p<(1<<m); p++) {
			if(p&(p<<1))continue;
			if(p&(p<<2))continue;
			if(j&p)continue;
			if(p&(~a[1]))continue;
			f[2][j][p]=king[j]+king[p];
		}
	}
	//状态转移
	for(int i=3; i<=n; i++) {
		for(int j=0; j<(1<<m); j++) {
			if(j&(j<<1))continue;
			if(j&(j<<2))continue;
			if(j&(~a[i]))continue; 
			for(int p=0; p<(1<<m); p++) {
				if(p&(p<<1))continue;
				if(p&(p<<2))continue;
				if(j&p)continue;
				if(p&(~a[i-1]))continue;
				for(int q=0; q<(1<<m); q++) {
					if(q&(q<<1))continue;
					if(q&(q<<2))continue;
					if(j&q)continue;
					if(p&q)continue;
					if(q&(~a[i-2]))continue;
					f[i%3][j][p]=max(f[i%3][j][p],f[(i-1)%3][p][q]+king[j]);
				}
			}
		}
	}
	long long ans=0;
	for(int j=0;j<(1<<m);j++){
		for(int p=0;p<(1<<m);p++){
			ans=max(f[n%3][j][p],ans);
		}
	}
	cout<<ans;
}

例题4:地图放炮2

例题链接

  • 题目描述: n × m n\times m n×m 的矩阵中放置若干个炮,使得炮与炮之间不会互相攻击到。能攻击到当且仅当两个炮之间有一个棋子。求合法放置的方案数。n,m<=100
  • 问题分析:

例题5:地图摆马

例题链接

  • 题目描述: X × Y X\times Y X×Y 的方阵中,放入任意个中国象棋中的马。求马之间不会互相攻击到的情况的方案数。 X ∈ [ 1 , 100 ] , Y ∈ [ 1 , 6 ] X\in[1,100],Y\in[1,6] X[1,100],Y[1,6]
  • 问题分析: 主要是马脚很麻烦,对于相邻的两行 j , k j,k j,k ,考虑暴力去检查是否会互相攻击到。对于相邻的三行 j , k , p j,k,p j,k,p 也一样。
#include<bits/stdc++.h>
using namespace std;

long long f[3][65][65],mod=1e9+7,X,Y;

int check1(int x,int y){//吃到上一层 
	for(int i=2;i<Y;i++){
		if(x&(1<<i)){
			if(x&(1<<(i-1)))continue;
			else if(y&(1<<(i-2)))return 1;
		}
	}
	for(int i=0;i<Y-2;i++){
		if(x&(1<<i)){
			if(x&(1<<(i+1)))continue;
			else if(y&(1<<(i+2)))return 1;
		}
	}
	for(int i=2;i<Y;i++){
		if(y&(1<<i)){
			if(y&(1<<(i-1)))continue;
			else if(x&(1<<(i-2)))return 1;
		}
	}
	for(int i=0;i<Y-2;i++){
		if(y&(1<<i)){
			if(y&(1<<(i+1)))continue;
			else if(x&(1<<(i+2)))return 1;
		}
	}
	return 0;
}
int check2(int x,int y,int z){//吃到上上层 
	for(int i=1;i<Y;i++){
		if(x&(1<<i)){
			if(y&(1<<i))continue;
			else if(z&(1<<(i-1)))return 1;
		}
	}
	for(int i=0;i<Y-1;i++){
		if(x&(1<<i)){
			if(y&(1<<i))continue;
			else if(z&(1<<(i+1)))return 1;
		}
	}
	for(int i=1;i<Y;i++){
		if(z&(1<<i)){
			if(y&(1<<i))continue;
			else if(x&(1<<(i-1)))return 1;
		}
	}
	for(int i=0;i<Y-1;i++){
		if(z&(1<<i)){
			if(y&(1<<i))continue;
			else if(x&(1<<(i+1)))return 1;
		}
	}
	return 0;
}
int main() {
	cin>>X>>Y;
	if(X==1) {
		cout<<(1<<Y);
		return 0;
	}
	for(int j=0; j<(1<<Y); j++) {
		for(int k=0; k<(1<<Y); k++) {
			if(check1(j,k))continue;
			f[2][j][k]=1;
		}
	}
	for(int i=3; i<=X; i++) {//100*64*64*64
		for(int j=0; j<(1<<Y); j++) {
			for(int k=0; k<(1<<Y); k++) {
				if(check1(j,k))continue;//吃到上一层 
				for(int p=0; p<(1<<Y); p++) {
					if(check2(j,k,p))continue;//吃到上上层 
					f[i%3][j][k]=(f[i%3][j][k]+f[(i-1)%3][k][p])%mod;
				}
			}
		}
	}
	long long ans=0;
	for(int j=0; j<(1<<Y); j++) {
		for(int k=0; k<(1<<Y); k++) {
			ans=(ans+f[X%3][j][k])%mod;
		}
	}
	cout<<ans;
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值