状压dp小结

状压dp的引入

状压DP,是用二进制的性质来描述状态的一种DP。

对于状压dp,我们要先了解一下位运算。

位运算

  • x&y 与运算,101&110=100
  • x|y 或运算,100|101=101
  • x^y 异或运算,101^100=001
  • x<<1 左移运算
  • x>>1 右移运算

状压dp

先看一道题:

\(n\times n\) 的棋盘上放 \(k\) 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。
对于全部数据,\(1≤n≤10,0≤k≤n^2\)

方法一:爆搜

时间复杂度:\(O(2^{n^2})\)

方法二:状压dp

那dp的状态是什么?

要表示每行的状态,和第几行,是不是要开 dp[11][2][2][2][2][2][2][2][2][2][2]

那就非常麻烦。

于是考虑压维。

我们发现后面的维度都是2,那是不是很像...

二进制!

对,我们可以把后面的维数压成二进制!

然后原先的每个状态现在对应二进制的每一位!

然后我们就能巧妙解决了这个问题!

\(dp[i][j][s]\) 表示第 \(i\) 行放置方式为 \(s\)\(i\) 行放置 \(j\) 个国王的方案数。

那,二进制怎么转移啊?

我们发现,国王不能左右攻击,也就是 s&(s<<1)==0

国王不能上下攻击,就是 s&c==0

国王不能斜着攻击,就是 s&(c<<1)=0s&(c>>1)=0

那当满足上述条件的时候,是不是就可以从 \(dp[i-1][j-qiu(s)][c]\) 转移到 \(dp[i][j][s]\) 了?

然后似乎就做出来了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n, m, i, j, k; 
int dp[15][150][1050]; 
int ans, a, b; 

int qiu(int x) //求这个状态有多少个国王 
{
	int ans=0; 
	while(x) ++ans, x-=x&-x; 
	return ans; 
}

signed main()
{
	scanf("%lld%lld", &n, &k); 
	for(i=0; i<(1<<n); ++i)
		if((m=qiu(i))<=k && !(i&(i<<1)))
			dp[1][m][i]=1; //初始化第一行
	for(i=2; i<=n; ++i) //第几行 
		for(a=0; a<(1<<n); ++a) //这一行状态 
			for(b=0; b<(1<<n); ++b) //上一行状态 
				if(!(a&b) && !(a&(b<<1)) && !(a&(b>>1)) && !(b&(b<<1))) //不互相攻击 
					for(j=(m=qiu(b))+qiu(a); j<=k; ++j) //放置国王个数 
						dp[i][j][b]+=dp[i-1][j-m][a]; 
	for(i=0; i<(1<<n); ++i) ans+=dp[n][k][i]; //计算答案 
	printf("%lld", ans); 
	return 0;
}

这道题不够经典,我们来看一道经典题

给出一个由01组成网格,可以在1的位置上放置物品,当放置的东西不能有公共边,问有多少种放法。
\(1≤N,M≤12\)

这题不同的是,有些位置不能放置物品。

那我们可以预处理每一行哪些位置不能放置,记为 \(a_i\)

然后你在第 \(i\) 行放置的状态 \(s\) 必须满足 s&a_i=0

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mo 100000000
#define N 15
#define M 100010
int n, m, i, j, k; 
int a[N], f[N][M], ans; 
int flg[M], maxx; 

signed main()
{
	scanf("%lld%lld", &n, &m); 
	for(i=1; i<=n; ++i)
		for(j=1; j<=m; ++j) 
			scanf("%lld", &k), a[i]=(a[i]<<1)+k; //预处理每一行是否能放 
	f[0][0]=1; maxx=(1<<m); 
	for(i=0; i<maxx; ++i) 
		if((i&(i<<1))==0&&(i&(i>>1))==0) flg[i]=1; //预处理状态 
	for(i=1; i<=n; ++i) //第几行 
		for(j=0; j<maxx; ++j) //这一行状态 
			if((j&a[i])==j&&flg[j]) //满足可以放 
				for(k=0; k<maxx; ++k) //上一行状态 
					if((j&k)==0) f[i][j]=(f[i][j]+f[i-1][k])%mo; 
	for(i=0; i<(1<<m); ++i) ans=(ans+f[n][i])%mo; //统计答案 
	printf("%lld", ans); 
	return 0;
}

刚刚的问题我们可以进行更深入的思考。

刚刚的问题的放置影响区域是这样:
image
那如果是这样呢:
image

这个问题就变得非常有趣了。

我们不妨加一个状态,由 \(dp[i][j]\) 变成 \(dp[i][j][k]\),表示上两行的状态。

这样子,我们也可以利用状压dp轻松转移。

但是,这时间复杂度承受得了吗?

表面上看来,状态+转移必然会爆,但实际上并不会。

在一行中放置间隔至少为2的物品,情况非常小。

我们考虑所以数据范围至 \(n\leqslant 100, m\leqslant 10\),那似乎就能做出这么一道有趣的题了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n, m, i, j, k; 
int dp[3][1025][1025]; 
int ans, u, a[110]; 
char s[15]; 

int check(int x, int i)
{
	if(((x&(x<<1)) || (x&(x<<2)))) return 1; 
	if((x|a[i])!=a[i]) return 1; 
	return 0; 
}

int suan(int x)
{
	int ans=0; 
	while(x) ++ans, x-=x&-x; 
	return ans; 
}

signed main()
{
	memset(dp, -1, sizeof(dp)); 
	scanf("%lld%lld", &n, &m); 
	for(i=1; i<=n; ++i)
	{
		scanf("%s", s); 
		for(j=0; j<m; ++j)
			a[i]|=((s[j]=='P' ? 1 : 0)<<j); 
	}
	for(i=0; i<(1<<m); ++i)
	{
		if(check(i, 2)) continue; 
		for(j=0; j<(1<<m); ++j)
			if(!check(j, 1) && (i&j)==0) dp[0][i][j]=suan(i)+suan(j); 
	}
	for(i=3; i<=n; ++i)
		for(j=0; j<(1<<m); ++j)
		{
			if(check(j, i)) continue; 
			for(k=0; k<(1<<m); ++k)
			{
				if(check(k, i-1) || (j&k)) continue; 
				for(u=0; u<(1<<m); ++u)
				{
					if(check(u, i-2) || (k&u) || (j&u)) continue; 
					dp[i%2][j][k]=max(dp[i%2][j][k], suan(j)+dp[(i-1)%2][k][u]); 
				}
			}	
		}
	for(i=0; i<(1<<m); ++i)
	{
		if(check(i, n)) continue; 
		for(j=0; j<(1<<m); ++j)
			if((!check(j, n-1)) && ((i&j)==0)) ans=max(ans, dp[n%2][i][j]); 
	}
	printf("%lld", ans); 
	return 0;
}

其实这道题,不仅有这种方法,同样可以压成一维,这就留给读者自己思考。

然而,有些时候,某些题不能压成二进制,是不是就不能用状压dp呢?

我们同样考虑刚刚那类题目,加入状态不是放或不放,而是 \(A,B,C\) 三种状态,那行不行呢?

我们状压dp,是压成二进制。那么此时有三种状态,我们就压成三进制。

实现过程可以参考一下代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 100010
#define mo 1000000
int n, m, i, j, k, p; 
int f[N], dp[N], t[N]; 

int check(int x)
{
	for(int i=1; i<m; ++i)
	{
		if(x%3==x/3%3) return 0; 
		x/=3; 
	}
	return 1; 
}

int pan(int x, int y)
{
	for(int i=1; i<=m; ++i, x/=3, y/=3)
		if(x%3==y%3) return 0; 
	return 1; 
}

int suan(int x)
{
	if(x==0) return 1; 
	memset(f, 0, sizeof(f)); 
	f[j]=1; 
	int i, j, k; 
	for(k=1; k<=x; ++k)
	{
		for(i=0; i<t[m]; ++i)
			if(check(i))
				for(j=0; j<t[m]; ++j)
					if(check(j) && pan(i, j))
							dp[i]+=f[j]; 					
		for(i=0; i<t[m]; ++i) f[i]=dp[i]%mo, dp[i]=0; 
	}
	for(i=k=0; i<t[m]; ++i) k+=f[i]; 
	return k%mo; 
}

signed main()
{
	scanf("%lld%lld%lld", &n, &m, &k); 
	for(i=t[0]=1; i<=10; ++i) t[i]=t[i-1]*3%mo; 
	for(i=1; i<=m; ++i) scanf("%lld", &p), j+=t[i-1]*(p-1); 
	printf("%lld", suan(k-1)*suan(n-k)%mo); 
	return 0;
}

例题

让我们来看一道例题:

[APIO2007] 动物园

给出一个圆环,每个人能看到从某个位置开始连续的五只动物。

对于一个人,如果任意一个自己讨厌的动物被移走,或者任意一个自己喜欢的动物没被移走,那么他就很高兴。

请问最多能使多少人开心。

对于 \(100\%\) 的数据,\(10 \le N \le 10^4\)\(1 \le C \le 5\times 10^4\)\(1 \le E \le N\)

由于动物数和人数很多,明显不能状压,而每个人看到的动物很少,所以可以状压。

由于是个环,我们就先破环成链,设 \(dp[i][j]\) 为从第 \(i\) 个位置开始后五个位置状态为 \(j\) 能使站在前 \(i\) 个位置的人开心的最多数目。

考虑一下这副图:

image

我们当前枚举的是黄色部分,而我们需要从紫色部分转移过来。

那么对于 \(i-1\) 的那个位置,我们可以枚举它是1或0。

于是我们可以把黄色部分的前面拿出来,变成紫色部分,进行转移。

然而,又有一个问题,环的情况怎么办?

其实,对于环,我们可以枚举初始状态,对于每种初始状态做一次dp。

时间复杂度:\(O(n\times 2^{10})\)

#include<bits/stdc++.h>
using namespace std; 
#define int long long
int n, m, i, j, k; 
int dp[10010][50]; 
int f[10010][50]; 
int p, ans, l, r, e; 
int a, b; 

int du()
{
	int x; scanf("%lld", &x); 
	if(x<e) x+=n; 
	return x-=e; 
}

int check(int x, int a, int b)
{
	if(x&b) return 1; 
	if((~x)&a) return 1; 
	return 0; 
}

signed main()
{
	memset(dp, 0x80, sizeof(dp)); 
	scanf("%lld%lld", &n, &m); 
	for(i=1; i<=m; ++i)
	{
		scanf("%lld%lld%lld", &e, &l, &r); 
		b=0; a=0; 
		for(j=1; j<=l; ++j) a|=(1<<du()); 
		for(j=1; j<=r; ++j) b|=(1<<du()); 
		for(j=0; j<32; ++j) f[e][j]+=check(j, a, b); 
	}
	for(p=0; p<32; ++p)
	{
		memset(dp[0], 0x80, sizeof(dp[0])); 
		dp[0][p]=0; 
		for(i=1; i<=n; ++i) 
			for(j=0; j<32; ++j)
				dp[i][j]=max(dp[i-1][(j&15)<<1], dp[i-1][(j&15)<<1|1])+f[i][j]; 
		ans=max(ans, dp[n][p]); 
	}
	printf("%lld", ans); 
	return 0;
}

以上例题对应一本通提高篇题目。

posted @ 2022-01-20 11:43  zhangtingxi  阅读(42)  评论(0编辑  收藏  举报