🧠 一、什么是状压 DP?
状压 DP(State Compression Dynamic Programming)是将状态用二进制数来表示,从而将多个布尔状态压缩成一个整数,用位运算来操作状态,以节省空间和提高效率的动态规划方法。
适用场景:
- 状态数量不多,但状态组合复杂。
- 每个元素只有“选”或“不选”两种状态(0/1 状态)。
- 需要枚举子集、判断状态转移等操作。
- 常见于:TSP 旅行商问题、棋盘覆盖、集合覆盖、排列组合类问题等。
🧩 二、核心思想:用二进制表示状态
1. 二进制表示集合
假设我们有 n 个物品或位置,我们可以用一个 n 位的二进制数来表示某个状态:
- 第
i位为 1:表示第i个元素被选中或已处理。 - 第
i位为 0:表示第i个元素未被选中或未处理。
例如,n = 4,状态 1011 表示:
- 第 0 个被选
- 第 1 个被选
- 第 2 个未被选
- 第 3 个被选
对应的十进制是 1+2+0+8 = 11
所以状态 1011 可以用整数 11 表示。
🔧 三、常用位运算技巧
在状压 DP 中,熟练掌握位运算是关键:
| 操作 | 语法 | 说明 |
|---|---|---|
取第 i 位 | (state >> i) & 1 | 判断第 i 位是否为 1 |
将第 i 位设为 1 | state | (1 << i) | 选中第 i 个元素 |
将第 i 位设为 0 | state & ~(1 << i) | 取消选中第 i 个元素 |
判断是否包含某个子集 s | (state & s) == s | s 是 state 的子集 |
| 枚举子集 | for(int s = state; s; s = (s-1) & state) | 高效枚举所有子集 |
📚 四、经典例题详解:TSP 旅行商问题(Traveling Salesman Problem)
问题描述:
给定 n 个城市(编号 0 ~ n-1),以及一个 n * n 的距离矩阵 dist[i][j],表示从城市 i 到城市 j 的距离。
求从城市 0 出发,经过所有城市恰好一次,最后回到城市 0 的最短路径长度。
1. 状态设计
我们定义:
dp[mask][i]
mask:一个整数,表示当前已经访问过的城市集合(二进制表示)。i:当前所在城市(必须在mask中为 1)。dp[mask][i]:表示在已访问集合为mask,且当前位于城市i的情况下,所需的最小路径长度。
2. 初始状态
从城市 0 开始:
dp[1 << 0][0] = 0;
即:只访问了城市 0,当前在城市 0,路径长度为 0。
3. 状态转移
我们遍历所有可能的状态 mask,对于每个状态中的城市 i,尝试从 i 转移到一个未访问的城市 j。
转移方程:
if ((mask >> j) & 1) continue; // j 已访问,跳过
int new_mask = mask | (1 << j);
dp[new_mask][j] = min(dp[new_mask][j], dp[mask][i] + dist[i][j]);
4. 最终答案
当 mask == (1 << n) - 1(即所有城市都访问过),我们从任意城市 i 返回城市 0:
ans = min(ans, dp[(1<<n)-1][i] + dist[i][0]);
5. C++ 实现代码
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 16;
const int MAXMASK = 1 << MAXN;
const int INF = 0x3f3f3f3f;
int n;
int dist[MAXN][MAXN];
int dp[MAXMASK][MAXN]; // dp[mask][i]: 当前状态为 mask,位于城市 i 的最小代价
int solve() {
int full_mask = (1 << n) - 1; // 所有城市都访问的状态
// 初始化 dp 数组为无穷大
memset(dp, 0x3f, sizeof(dp));
// 初始状态:从城市 0 出发
dp[1 << 0][0] = 0;
// 枚举所有状态
for (int mask = 0; mask <= full_mask; mask++) {
for (int i = 0; i < n; i++) {
if (dp[mask][i] == INF) continue; // 无效状态跳过
if (!((mask >> i) & 1)) continue; // 状态不包含城市 i,非法
// 尝试从城市 i 转移到所有未访问的城市 j
for (int j = 0; j < n; j++) {
if ((mask >> j) & 1) continue; // j 已访问
int new_mask = mask | (1 << j);
dp[new_mask][j] = min(dp[new_mask][j], dp[mask][i] + dist[i][j]);
}
}
}
// 找最终答案:从任意城市 i 回到城市 0
int ans = INF;
for (int i = 1; i < n; i++) {
if (dp[full_mask][i] != INF) {
ans = min(ans, dp[full_mask][i] + dist[i][0]);
}
}
return ans;
}
int main() {
cout << "请输入城市数量 n: ";
cin >> n;
cout << "请输入 " << n << "x" << n << " 的距离矩阵(对称矩阵):" << endl;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> dist[i][j];
}
}
int result = solve();
if (result == INF) {
cout << "无法找到有效路径!" << endl;
} else {
cout << "最短旅行商路径长度为: " << result << endl;
}
return 0;
}
🎯 五、其他经典应用举例
1. 棋盘覆盖问题(炮兵阵地)
- 棋盘上某些位置不能放炮兵,炮兵攻击范围为上下左右两格。
- 问最多能放多少炮兵。
思路:
- 用
dp[i][mask][prev_mask]表示第i行状态为mask,第i-1行为prev_mask时的最大数量。 - 枚举合法状态(不能相邻两个炮兵),并判断是否与地形冲突。
2. 集合划分问题(子集和最小最大)
- 将集合划分为
k个子集,使最大子集和最小。 - 可用状压 DP 预处理所有子集的和,再进行分组 DP。
3. Hamilton 路径计数
- 求从起点到终点,经过所有点恰好一次的路径数。
- 类似 TSP,但记录的是方案数而非最短路径。
🛠 六、优化技巧
1. 空间优化
如果状态只依赖前一层,可以使用滚动数组减少空间。
2. 预处理合法状态
例如在棋盘问题中,提前预处理出所有“不冲突”的状态(如没有相邻的 1),减少枚举量。
vector<int> valid_states;
for (int s = 0; s < (1 << n); s++) {
if ((s & (s << 1)) == 0 && (s & (s << 2)) == 0) { // 无相邻炮兵
valid_states.push_back(s);
}
}
3. 使用 __builtin_popcount 计算二进制中 1 的个数
int cnt = __builtin_popcount(state); // 返回 state 中 1 的个数
🧪 七、调试技巧
- 打印状态转移过程,观察
mask和i的变化。 - 使用小样例(如
n=3)手动模拟状态转移。 - 注意边界条件:初始状态、非法状态跳过、最终状态判断。
✅ 八、总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 用二进制整数表示集合状态 |
| 关键技巧 | 位运算操作(取位、置位、子集枚举) |
| 状态设计 | dp[mask][...],mask 表示已处理集合 |
| 典型问题 | TSP、棋盘覆盖、集合划分、Hamilton 路径 |
📌 九、常见错误与注意事项
- 数组大小不够:
dp[1<<n][n],注意1<<n是2^n,不要写成1<<n在栈上定义过大。 - 未初始化为 INF:导致状态被错误更新。
- 忽略状态合法性:如
mask不包含i却访问dp[mask][i]。 - 边界处理错误:如 TSP 忘记返回起点。
- 位运算优先级:
>>和&优先级较低,记得加括号:(mask >> i) & 1。
🧰 十、扩展阅读
- 轮廓线 DP(插头 DP):更复杂的状压形式,用于棋盘路径问题。
- 快速子集卷积:用于集合卷积类问题,高级技巧。
1万+

被折叠的 条评论
为什么被折叠?



