状态压缩 DP
状态压缩 DP
概念
状态压缩DP(又称集合动态规划,以下简称状压DP),是一种以集合状态为状态解决问题的动态规划算法。
什么是以集合为状态?
举个常用的例子:集合内每个元素的使用情况。这类似搜索中的标记数组(使用过被标记,防止重复使用)。但搜索中的标记数组是一个数组,而在状压DP中使用很多个数组储存所有使用情况非常消耗空间。这时便有了状态压缩。
基本思想
状态压缩
依然使用使用集合内每个元素的使用情况为例:
观察标记数组 \(book =\{0,1,0,1,0,0,0,1\}\),其中 \(1\) 代表已使用, \(0\) 代表未使用。
将所有 \(0\) 和 \(1\) 按顺序紧密排列在一起就会得到一个 \(01\) 串:\(01010001\),容易发现这代表一个十进制数 \(81\) 的二进制形式。这样我们就成功把一个标记数组转换为了一个数字。
位操作
如果想要像操作数组一样操作这个二进制数,我们就会运用到位操作:
-
右移:
x >> y,本质上是删除一个二进制数 \(x\) 的右端 \(y\) 个数字,整体向右移动( 即\(\cfrac{x}{2^y}\)),如:12 >> 2\(\Rightarrow\)二进制(1100) >> 2\(=\)二进制(0011)\(\Rightarrow\)3\(\cfrac{12}{2^2} = \cfrac{12}{4} = 3\)
-
左移:
x << y,与右移相反本质上是在一个二进制数 \(x\) 的右端加上 \(y\) 个 \(0\),整体向左移动(即 \(x\times 2^y\)),如:3 << 2\(\Rightarrow\)二进制(0011) << 2\(=\)二进制(1100)\(\Rightarrow\)3\(3 \times 2^2 = 3 \times 4 = 12\)
-
按位与:
x & y,将 \(x\) 与 \(y\) 的每一位比较。如果都为 \(1\),答案的这一位也为 \(1\),反之则为 \(0\) ,如:3 & 6\(\Rightarrow\)二进制(0011) & 二进制(0110):x = \(3\) 0 0 1 1 y = \(6\) 0 1 1 0 ans 0 0 1 0 \(ans = (0010)_2 = 2\)
即
3 & 6 = 2 -
按位或:
x | y,同理,将 \(x\) 与 \(y\) 的每一位比较。如果至少有一个为 \(1\),答案的这一位也为 \(1\),反之则为 \(0\) 。 -
按位非:
~x,同理,将 \(x\) 的每一位取到相反的数。如果为 \(1\),则答案的这一位为 \(0\),反之则为 \(1\) 。
对于当前状态 \(s\) ,我们会有以下操作:
- 查询第 \(i\) 位的值,如果
(s & (1 << i))为真(答案不为 \(0\))则第 \(i\) 位为 \(1\) ,反之则为 \(0\) - 将第 \(i\) 位设为 \(1\) ,
s | (1 << i),只将 \(i\) 位设为 \(1\) ,再按位或 - 将第 \(i\) 位设为 \(0\) ,
s & ~(1 << i),除第 \(i\) 位外,其余为均为 \(1\) , 按位与后不变
算法实现
我们先来看一道简单的题目,洛谷P1896 [SCOI2005] 互不侵犯:
在 \(N \times N\) 的棋盘里面放 \(K\) 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上,下,左,右,左上,左下,右上,右下八个方向上附近的各一个格子,共 \(8\) 个格子。(\(1\leq N\leq 9\))
看到这个问题很容易联想到经典的八皇后问题,但这是国王(攻击范围很小),所以方案也变得很多,搜索无法完成:
我们用状压DP,设定DP状态 \(dp[i,s,k]\) 表示第 \(i\) 行状态为 \(s\) 已放置 \(k\) 个国王的方案总数,我们可以得到它的状态转移方程:
这里 \(s_0\) 表示上一行的状态,\(k_0\) 表示截止上一行共选了几个
当 \(N = 9\) 时:时间复杂度近似为 \(O(9\times 9^2 \times 81 \times 81)\),即 \(O(6 \times 10^6)\)
所以状压DP的数据范围的某一维通常会很小
代码实现:
-
初始化所有可能状态:
int bookcnt = 0; // 可能状态 int book[(1 << maxn) + 5],num[(1 << maxn) + 5]; // 可能状态以及这个状态有几个国王(几个1) void init() { for (int i = 0;i < (1 << n);i ++) { // 枚举状态 if (i & (i << 1)) continue; // 与前一位冲突(左右能攻击到) bookcnt ++; // 新方案 book[bookcnt] = i; // 具体方案十进制数 for (int j = 0;j < maxn;j ++) if (i & (1 << j)) num[bookcnt] ++; // 含1量 } } -
动态规划主体:
for (int i = 1;i <= n;i ++) // 枚举行 for (int si = 1;si <= bookcnt;si ++) // 这一行的状态 for (int sn = 0;sn <= k;sn ++) // 枚举剩余国王数量 if (sn >= num[si]) // 剩余国王数量足够 for (int sj = 1;sj <= bookcnt;sj ++) // 上一行的状态 if ( !(book[si] & book[sj]) && // 与上方无冲突 !(book[sj] & (book[si] << 1)) && // 与左斜方无冲突 !(book[sj] & (book[si] >> 1)) // 与右斜方无冲突 ) dp[i][si][sn] += dp[i - 1][sj][sn - num[si]]; // 方案数增加 -
统计答案:
ll ans = 0; for (int i = 1;i <= bookcnt;i ++) ans += dp[n][i][k]; // 最后一行的所有情况 printf("%lld",ans);
完整代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 9;
int n,k;
ll dp[maxn + 5][(1 << maxn) + 5][maxn * maxn + 5];
int bookcnt = 0;
int book[(1 << maxn) + 5],num[(1 << maxn) + 5];
void init() {
for (int i = 0;i < (1 << n);i ++) {
if (i & (i << 1)) continue;
int cnt = 0;
for (int j = 0;j < maxn;j ++) if (i & (1 << j)) cnt ++;
bookcnt ++;
book[bookcnt] = i;
num[bookcnt] = cnt;
}
}
int main() {
scanf("%d %d",&n,&k);
init();
dp[0][1][0] = 1ll;
for (int i = 1;i <= n;i ++)
for (int si = 1;si <= bookcnt;si ++)
for (int sn = 0;sn <= k;sn ++)
if (sn >= num[si])
for (int sj = 1;sj <= bookcnt;sj ++)
if (!(book[si] & book[sj]) && !(book[sj] & (book[si] << 1)) && !(book[sj] & (book[si] >> 1)))
dp[i][si][sn] += dp[i - 1][sj][sn - num[si]];
ll ans = 0;
for (int i = 1;i <= bookcnt;i ++) ans += dp[n][i][k];
printf("%lld",ans);
return 0;
}
扩展
三进制状态压缩
三进制状态压缩与二进制状态压缩类似:
- 获取 \(x\) 的第 \(i\) 位,利用进制的特性,这一位的值为 \(\cfrac{x}{3^k}~mod~3\)
- 更新 \(x\) 的第 \(i\) 位为 \(v\),\(x + (v-(\cfrac{x}{3^k}~mod~3))·3^k\)
除此之外,其余思路几乎相同
例题
洛谷P2704 [NOI2001] 炮兵阵地:与前两排都有关系

浙公网安备 33010602011771号