状压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
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<>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<

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

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

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

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

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

#include
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<>1))==0) flg[i]=1; //预处理状态 
	for(i=1; i<=n; ++i) //第几行 
		for(j=0; j

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

刚刚的问题的放置影响区域是这样:

那如果是这样呢:

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

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

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

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

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

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

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

#include
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

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

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

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

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

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

#include
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

例题

让我们来看一道例题:

[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\) 个位置的人开心的最多数目。

考虑一下这副图:

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

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

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

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

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

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

#include
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

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