用于dp的状态压缩思想

Posted by 许大仙 on June 12, 2019

状态压缩

适用范围

维数很多+总量很少时,可以将状态压缩为一个数来记录 【一般就是有n<20,n<12这种很小的范围的时候,要考虑到状态压缩】 参考:https://www.cnblogs.com/shzr/p/9064737.html

状态压缩就是把很多维度的状态,压缩成一个数值来表示。比如用二进制/十进制等的方式来存储状态,把状态变成一个数字。

例如:vis[x]=1,表示访问过这个状态x。

通过化状态为数组【把多维度降为1维】,可以对这个状态进行dp、bfs、dfs搜索等

状态压缩DP

1.玉米田 洛谷P1879

①题目描述

农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。

遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。

John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)

链接:https://www.luogu.org/problemnew/show/P1879

②解题思路

对于很多小图【小棋盘等】的搜索都是用可以用状态压缩来做的。把整个例如3*3的棋盘状态用那个9位十进制数/二进制数来存储。

类似地,这里有:(1 ≤ M ≤ 12; 1 ≤ N ≤ 12)。

很显然,这是总范围小,但是维度大的情况。要考虑状态压缩

但是12*12=144来表示一个状态显然太大了,不论是二进制还是几进制。

因此要考虑行转移,把一行作为一个状态,进行行压缩

状态转移方程

以一个二进制数表示一个行状态,1表示种了草,0表示没有。 dp[i][j] 表示考虑前i行,第i行状态为j可行的总方案数。

因此状态转移方程为:

dp[i][j]=∑dp[i−1][k]【其中j,k两状态都合法】

可以理解为:前i-1行中的所有合法状态【遍历k】的方案和等于在第i行放置合法状态j【j和k状态位于第i行和第i-1行可相容,即无相邻草地】可达到的前i行可行方案总数。

则最后ans=∑dp[n][j] 【其中 j遍历每个状态,即0<=j<=(1<<m)−1且j合法】

空间优化:合法状态j->第j个合法状态

考虑到状态转移的时候每次都要枚举2^m个状态【每行有m个列】 其中有些状态本身就不合法(即相邻位置种了草),这样会造成无用枚举 所以可以一开始先预处理出只考虑不相邻的所有合法方案,大大减少枚举量。

这样dp[i][j]中的一维维度就会下降。本来j位置应该开辟2^m大小的空间。

如果我们做一个预处理,把dp[i][j]重新定义为第j个合法状态【而不是合法状态j】,就可以大幅减少空间消耗。【将具体状态存储到一个新的数组中(rem[j]),用j索引到具体状态】

如果2^m中,抛除相邻位置种了草的方案,剩下的合法状态会大大减少为k,那么dp[i][j]中j位置只需要k大小的空间。

合法性判断

(1)行内相邻:关于状态x是否合法的判断

//状态x表示某行的状态,二进制位为1表示种了草
if( (( x&(x<<1) )==0)  && (( x&(x>>1) )==0) ) 
    return true;
else 
    return false;

即将x分别左移和右移,若x二进制中某相邻两位皆为1,则左/右移后两位重合,&运算必定返回1,表示不合法。

必须最多是……010101……交替,再移动一位以后才使得x&(x«1)=0

(2)土地是否可种:判断状态x是否与土地情况冲突

在读入时记录st[i],表示第i行的土地情况,具体每个位,用1表示能种,0表示不能种。那么:

if((~st[i])&rem[i]) return 0

将st每位取反,那么则1表示不能种,&运算后返回不等于0,说明状态x一定在不合法的位置种了草【有某个已种草位和贫瘠土地都为1,&之后返回非0】。

(3)列内相邻

//rem[j]为第i行的状态,rem[k]为第i-1的行的状态
if(rem[k]&rem[j]!=0)//上下相邻,列内相邻。 
	continue;
复杂度

dp[i][j]=∑dp[i−1][k]【其中j,k两状态都合法】

复杂度:O(2^(2∗m)∗n)

其中,两个带2的次方的是当前行的合法状态枚举和上一行的合法状态枚举=(2^m)*(2^m)=2^(m+m)=2^(2∗m),n是行数【往外层从i-1推出i】

③解题代码
#include<iostream>
#include<vector> 
#include<string.h>
#define MOD 100000000
using namespace std;
int st[15];
int rem[3100];//2^12=1024*4,优化后的合法数大概3100
int cnt;//可以试着打一下表,看看cnt的值,来估计合法数 
int n,m;
int dp[15][3100];
bool check(){
	cnt=0;
	for(int x=0;x<=(1<<m)-1;x++){
		if((x&(x<<1))!=0)
			continue;
		if((x&(x>>1))!=0)
			continue;
		rem[cnt++]=x;
	}
}
int main(){
	cin>>n>>m;
	memset(st,0,sizeof(st));
	memset(dp,0,sizeof(dp));
	for(int i=0;i<n;i++){
		for(int j=0;j<m;j++){
			int temp;
			cin>>temp;
			st[i]=st[i]|(temp<<j);	
		}
	}
	check();
	//cout<<cnt<<endl;
	
	//dp初始化
	for(int i=0;i<cnt;i++){
		if((~st[0])&rem[i])
			continue;
		dp[0][i]=1;
	} 
	for(int i=1;i<n;i++){
		for(int j=0;j<cnt;j++){
			if((~st[i])&rem[j])
				continue; 
			//在第i行为第j个合法状态时,前i-1行的、与j状态相容的、合法方案全部相加
			//得到第i行为第j个合法状态时,前i行的合法方案总数。
			//当然第i行可以有多个合法状态。 
			for(int k=0;k<cnt;k++){
				//注意!=和==的优先级比&高,因此运算要加括号 
				if((rem[k]&rem[j])!=0)//上下相邻,列内相邻。 
					continue;
				if((~st[i-1])&rem[k])
					continue;
				dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
			}
		}
	}
	int ans=0;
	for(int i=0;i<cnt;i++){
		ans=(ans+dp[n-1][i])%MOD;
	}
	cout<<ans<<endl;
	
	return 0;
}