DP专题-学习笔记:状态压缩 DP
1. 前言
状态压缩 DP,简称状压 DP,是一种 DP (废话)。
这种 DP 的特点就是通常与二进制有关(当然也可能是其他进制),通常复杂度为 2 的阶乘次级别。
状压 DP 的问题有两个鲜明的特征:
- 问题的数据规模特别小,2 的阶乘次可以通过。
- 题目通常都是选与不选两种选择,可以使用二进制串表示。
第 2 个是什么意思呢?
打个比方,现在有 5 盘菜在你面前,编号 1-5,你此时想吃 1,3,4 号菜,那么就可以将你做的选择表示为 10110 的二进制串。
在继续看下去之前,请先确保熟练掌握各种位运算的知识。不了解的读者可以参考 OI-wiki:位运算。
如果没有特殊说明,本文的所有 01 串全部视为二进制串。
2. 详解
对于状压 DP,尤其重要的一点是:搞清楚二进制串表示的意义是什么。
通常来讲,题中只有两种选择的物品就可以使用二进制串来表示。
比如这道题,对于每一行而言:每一个格子只可能放或不放,那么此时就可以用二进制串表示。
例如 时,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);
}
其中 表示第 个状态对应的二进制串, 表示第 个二进制串用了多少个 1。
现在已经知道了所有单行内合法的二进制串,那么接下来开始设计 DP。
设 表示在第 行, 行已经使用了 个 1,当前行的状态为 的方案数。
需要注意的是:
- 个 1 实际上就是 个国王。
- 注意第三位的 只是一个标号,真正的状态是 ,这样做是因为 可能很大,防止炸空间。
那么怎么转移呢?
对于第 行的转移,我们需要知道这样 3 个消息:
- 当行二进制串 。
- 上一行二进制串 。
- 上一行至第一行用了 个 1。
但是考虑到一个国王可能会影响到上面一行的拜访,我们需要过滤掉这些非法情况。
分为 3 种情况:
- 如下。
这种情况下为上一行国王在这一行国王正上方,判断方法为k:....1.... j:....1....
- 如下。
这种情况下为上一行国王在这一行国王左上角,判断方法为 ,利用右移运算转化成第 1 种情况。k:...1..... j:....1....
- 如下。
这种情况下为上一行国王在这一行国王右上角,判断方法为 ,同样利用左移运算转化成第 1 种情况。k:.....1... j:....1....
当上面三条有任意一条符合,即为非法状态。
这样过滤了所有非法状态之后,就可以愉快的转移啦!
转移方程如下:
其中 为合法状态。
最后答案为 。
初值:。
代码:
#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 很要命的一点就是空间限制。
根据理论推算,上面这道题的状态数为 ,这道题还好,但是在一些别的题目中就会 MLE。例子见『练习题』的博文的第一道练习题。链接后面有。
那么此时怎么办呢?
可以采用滚动数组的方式减小空间,比如第一道练习题,洛谷题解区绝大多数人都是采用滚动数组节省空间,但是其实有一种更好的方法。
将数据调到最大,看看有几个状态不是就好了?
比如上面这道题,当 时,状态数只有 89 个,完全不用开 空间,开 即可。
这个方法在卡状压 DP 的空间的时候尤其有用!
当然对于某些毒瘤题,两种方法都要用。
4. 练习题
练习题传送门:DP专题-专项训练:状压 DP
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具