算法学习笔记:状态压缩 DP

算法学习笔记:状态压缩 DP

1. 前言

状态压缩 DP,简称状压 DP,是一种 DP (废话)

这种 DP 的特点就是通常与二进制有关(当然也可能是其他进制),通常复杂度为 2 的阶乘次级别。

状压 DP 的问题有两个鲜明的特征:

  1. 问题的数据规模特别小,2 的阶乘次可以通过。
  2. 题目通常都是选与不选两种选择,可以使用二进制串表示。

第 2 个是什么意思呢?

打个比方,现在有 5 盘菜在你面前,编号 1-5,你此时想吃 1,3,4 号菜,那么就可以将你做的选择表示为 10110 的二进制串。

在继续看下去之前,请先确保熟练掌握各种位运算的知识。不了解的读者可以参考 OI-wiki:位运算

如果没有特殊说明,本文的所有 01 串全部视为二进制串。

2. 详解

例题:P1896 [SCOI2005]互不侵犯

对于状压 DP,尤其重要的一点是:搞清楚二进制串表示的意义是什么。

通常来讲,题中只有两种选择的物品就可以使用二进制串来表示。

比如这道题,对于每一行而言:每一个格子只可能放或不放,那么此时就可以用二进制串表示。

例如 n = 6 n=6 n=6 时,101001 表示第 1,3,6 列放,其余列不放。

那么首先我们需要一个 dfs 函数确定所有二进制串。

需要注意的是,在 dfs 中,为了接下来处理方便,我们需要尽可能的将题中限制条件加入 dfs 中以减少状态数和后面 DP 时的非法状态判断。

对于这道题,我们能够完成的就是在 dfs 的时候提前将相邻两项均为 1 的二进制串过滤掉。

代码:

void dfs(int pos, int num, int one)
{
    
    
	if (pos >= n) {
    
    State[++cnt] = num; sum[cnt] = one; return ;}
	dfs(pos + 1, num, one);
	dfs(pos + 2, num + (1 << pos), one + 1);
}

其中 S t a t e i State_i Statei 表示第 i i i 个状态对应的二进制串, s u m i sum_i sumi 表示第 i i i 个二进制串用了多少个 1。

现在已经知道了所有单行内合法的二进制串,那么接下来开始设计 DP。

f i , j , k f_{i,j,k} fi,j,k 表示在第 i i i 行, 1 − i 1-i 1i 行已经使用了 j j j 个 1,当前行的状态为 S t a t e k State_k Statek 的方案数。

需要注意的是:

  1. j j j 个 1 实际上就是 j j j 个国王。
  2. 注意第三位的 k k k 只是一个标号,真正的状态是 S t a t e k State_k Statek,这样做是因为 S t a t e k State_k Statek 可能很大,防止炸空间。
  3. j ≥ s u m k j \geq sum_k jsumk

那么怎么转移呢?

对于第 i i i 行的转移,我们需要知道这样 3 个消息:

  1. 当行二进制串 S t a t e j State_j Statej
  2. 上一行二进制串 S t a t e k State_k Statek
  3. 上一行至第一行用了 l l l 个 1。

但是考虑到一个国王可能会影响到上面一行的拜访,我们需要过滤掉这些非法情况。

分为 3 种情况:

  1. 如下。
    k:....1....
    j:....1....
    
    这种情况下为上一行国王在这一行国王正上方,判断方法为 j & k j \& k j&k
  2. 如下。
    k:...1.....
    j:....1....
    
    这种情况下为上一行国王在这一行国王左上角,判断方法为 ( j < < 1 ) & k (j<<1)\&k (j<<1)&k,利用右移运算转化成第 1 种情况。
  3. 如下。
    k:.....1...
    j:....1....
    
    这种情况下为上一行国王在这一行国王右上角,判断方法为 ( j > > 1 ) & k (j>>1)\&k (j>>1)&k,同样利用左移运算转化成第 1 种情况。

当上面三条有任意一条符合,即为非法状态。

这样过滤了所有非法状态之后,就可以愉快的转移啦!

转移方程如下:

f i , l + s u m j , j = ∑ f i − 1 , l , k ( l ≥ s u m k ) f_{i,l+sum_j,j}=\sum f_{i-1,l,k}(l \geq sum_k) fi,l+sumj,j=fi1,l,k(lsumk)

其中 j , k j,k j,k 为合法状态。

最后答案为 ∑ f n , k , i ∣ i ∈ [ 1 , c n t ] \sum f_{n,k,i}|i \in [1,cnt] fn,k,ii[1,cnt]

初值: f 1 , s u m i , i = 1 ∣ i ∈ [ 1 , c n t ] f_{1,sum_i,i}=1|i \in [1,cnt] f1,sumi,i=1i[1,cnt]

代码:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int MAXN = 9 + 5, MAXP = (1 << 9) + 10, MAXK = 9 * 9 + 10;
int n, p, State[MAXP], sum[MAXP], cnt;
LL f[MAXN][MAXK][MAXP];

int read()
{
    
    
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9' ; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
	return (fh == 1) ? sum : -sum;
}

void dfs(int pos, int num, int one)
{
    
    
	if (pos >= n) {
    
    State[++cnt] = num; sum[cnt] = one; return ;}
	dfs(pos + 1, num, one);
	dfs(pos + 2, num + (1 << pos), one + 1);
}

int main()
{
    
    
	n = read(), p = read();
	dfs(0, 0, 0);
	for (int i = 1; i <= cnt; ++i) f[1][sum[i]][i] = 1;
	for (int i = 2; i <= n; ++i)
		for (int j = 1; j <= cnt; ++j)
			for (int k = 1; k <= cnt; ++k)
			{
    
    
				if (State[j] & State[k]) continue;
				if ((State[j] << 1) & State[k]) continue;
				if ((State[j] >> 1) & State[k]) continue;
				for (int l = sum[j]; l <= p; ++l) f[i][l + sum[k]][k] += f[i - 1][l][j];
			}
	LL ans = 0;
	for (int i = 1; i <= cnt; ++i) ans += f[n][p][i];
	printf("%lld\n", ans);
	return 0;
}

3. 关于空间

状压 DP 很要命的一点就是空间限制。

根据理论推算,上面这道题的状态数为 2 n 2^n 2n,这道题还好,但是在一些别的题目中就会 MLE。例子见『练习题』的博文的第一道练习题。链接后面有。

那么此时怎么办呢?

可以采用滚动数组的方式减小空间,比如第一道练习题,洛谷题解区绝大多数人都是采用滚动数组节省空间,但是其实有一种更好的方法。

将数据调到最大,看看有几个状态不是就好了?

比如上面这道题,当 n = 9 n=9 n=9 时,状态数只有 89 个,完全不用开 2 n 2^n 2n 空间,开 89 + 10 89+10 89+10 即可。

这个方法在卡状压 DP 的空间的时候尤其有用!

当然对于某些毒瘤题,两种方法都要用。

4. 练习题

练习题传送门:DP算法总结&专题训练3(状压 DP)

猜你喜欢

转载自blog.csdn.net/BWzhuzehao/article/details/114286358