状压DP
一、什么是状态压缩动态规划(状压DP)
状态压缩动态规划(简称状压DP)是一种利用位运算高效表示和处理状态的动态规划方法。它通过将多维状态压缩到整数二进制位中,将原本难以处理的状态转化为位操作问题。
适用场景特征:
- 状态维度高但每维取值范围小(通常为布尔型)
- 状态之间存在可转移性
- 需要高效处理状态间关系
二、核心原理与位运算基础
1. 状态压缩原理
将n个元素的状态用n位二进制数表示:
- 第k位为1 → 第k个元素被选中
- 第k位为0 → 第k个元素未被选中
示例:5个元素的状态表示
10101 二进制 → 第0、2、4号元素被选中
2. 必备位运算技巧
| 操作 | 代码实现 | 示例(二进制) |
|---|---|---|
| 判断第k位是否为1 | mask & (1 << k) |
10101 & 00100 = 00100 |
| 设置第k位为1 | mask | (1 << k) |
10101 | 01000 = 11101 |
| 设置第k位为0 | mask & ~(1 << k) |
11101 & 11011 = 11001 |
| 切换第k位 | mask ^ (1 << k) |
10101 ^ 10000 = 00101 |
| 最低位的1 | mask & -mask |
10100 → 00100 |
| 统计1的个数 | __builtin_popcount(mask) |
10101 → 3 |
三、通用模板与框架
int n = 20; // 状态元素数量
int max_state = 1 << n; // 总状态数
vector<vector<int>> dp(max_state, vector<int>(n, INF));
// 初始化基础状态
for (int i = 0; i < n; ++i) {
dp[1 << i][i] = 0;
}
// 状态转移
for (int mask = 0; mask < max_state; ++mask) {
for (int i = 0; i < n; ++i) {
if (!(mask & (1 << i))) continue; // 当前状态不含i
for (int j = 0; j < n; ++j) {
if (mask & (1 << j))) continue; // 已包含j
int new_mask = mask | (1 << j);
dp[new_mask][j] = min(dp[new_mask][j],
dp[mask][i] + cost[i][j]);
}
}
}
四、经典例题详解
例题1:旅行商问题(TSP)
问题描述:给定n个城市(n ≤ 20)的坐标,求从起点出发访问所有城市恰好一次并返回起点的最短路径。
状态设计:
dp[mask][u]:已访问城市集合mask,当前位于城市u的最小花费
转移方程:
dp[mask | (1<<v)][v] = min(dp[mask | (1<<v)][v], dp[mask][u] + dis[u][v])
完整代码:
double tsp(vector<pair<int, int>>& cities) {
int n = cities.size();
vector<vector<double>> dis(n, vector<double>(n));
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
dis[i][j] = hypot(cities[i].first - cities[j].first,
cities[i].second - cities[j].second);
int max_state = 1 << n;
vector<vector<double>> dp(max_state, vector<double>(n, 1e18));
dp[1][0] = 0; // 起点为0号城市
for (int mask = 1; mask < max_state; ++mask) {
for (int u = 0; u < n; ++u) {
if (!(mask & (1 << u))) continue;
for (int v = 0; v < n; ++v) {
if (mask & (1 << v)) continue;
int new_mask = mask | (1 << v);
dp[new_mask][v] = min(dp[new_mask][v],
dp[mask][u] + dis[u][v]);
}
}
}
double ans = 1e18;
int full_mask = (1 << n) - 1;
for (int u = 1; u < n; ++u) // 最后返回起点0
ans = min(ans, dp[full_mask][u] + dis[u][0]);
return ans;
}
例题2:棋盘覆盖(POJ 2411)
问题描述:用1×2的骨牌铺满n×m的棋盘,求方案总数(1 ≤ n,m ≤ 11)。
状态设计:
dp[i][mask]:处理到第i行,当前行状态为mask的方案数- mask的二进制位表示该格是否被覆盖(需要与上一行状态配合)
关键技巧:
- 预处理所有合法行状态
- 判断相邻行状态的兼容性
状态转移:
for (int i = 1; i < n; ++i) {
for (int prev : valid_states) {
for (int curr : valid_states) {
if (check_compatible(prev, curr)) {
dp[i][curr] += dp[i-1][prev];
}
}
}
}
预处理合法状态:
vector<int> generate_states(int m) {
vector<int> states;
for (int mask = 0; mask < (1 << m); ++mask) {
bool valid = true;
for (int j = 0; j < m; ) {
if (mask & (1 << j)) {
++j;
} else {
if (j == m-1 || !(mask & (1 << (j+1)))) {
valid = false;
break;
}
j += 2;
}
}
if (valid) states.push_back(mask);
}
return states;
}
五、优化技巧与注意事项
- 滚动数组优化:当当前状态只依赖前一状态时,可用两个数组交替使用
vector<long long> prev(dp[0]);
vector<long long> curr(dp[0].size());
// 每轮交换prev和curr
- 按状态特征分组:根据状态中1的个数进行阶段转移
for (int cnt = 0; cnt <= n; ++cnt) {
for (auto mask : states_with_cnt[cnt]) {
// 处理该阶段的状态转移
}
}
-
预处理合法转移:预先计算每个状态的可能转移目标
-
剪枝优化:在循环中及时跳过不可能的状态
六、常见问题与解决方案
Q:当n较大(n>20)时怎么办?
A:考虑其他算法或优化方法,状压DP时间复杂度为O(n²2ⁿ),当n=20时2²⁰≈1e6,n=25时2²⁵≈3e7
Q:如何处理浮点数精度?
A:使用double类型,比较时设置epsilon(如1e-9)
Q:如何输出具体方案?
A:使用pre数组记录转移路径,逆向回溯
七、国王放置问题
【题目】
题目在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。
【转移方程】
\[f[i][j][l] += f[i - 1][x][l - (int) sta[j]]
\]
含义:
f[i][j][l]:表示 前i行,当前行为j,总共摆放l个国王的方案数。sta[j]:当前状态j放置的国王数量。- 转移公式:
f[i - 1][x][l - sta[j]]代表 上一行x状态,剩余l - sta[j]个国王 时的方案数。- 通过
+=叠加所有可能的合法x → j方案数。
【代码】
public class KingPlacement {
// sta: 记录每个合法状态的国王数量
// sit: 记录所有合法状态的二进制表示(位掩码)
static long[] sta = new long[2005], sit = new long[2005];
// f[i][j][l]: 代表前 i 行,当前行状态为 j,总共放置 l 个国王的方案数
static long[][][] f = new long[15][2005][105];
static int n, k, cnt; // n: 棋盘的列数, k: 需要放置的国王数量, cnt: 合法状态计数器
// 递归生成所有合法状态
public static void dfs(int x, int num, int cur) {
// x:当前状态的二进制表示(位掩码)。
// num:当前状态中的国王数量。
// cur:当前处理到的列位置。
if (cur >= n) { // 生成新的合法状态(遍历完所有列后)
sit[++cnt] = x; // 记录当前状态的位掩码
sta[cnt] = num; // 记录当前状态放置的国王数量
return;
}
dfs(x, num, cur + 1); // 当前列不放国王,继续递归下一列
// 当前列放国王(相邻列不可放置),所以跳过下一列,递归处理后面的列
dfs(x + (1 << cur), num + 1, cur + 2);
}
// 判断状态 j(当前行)与状态 x(上一行)是否兼容
public static boolean compatible(int j, int x) {
// 规则:上下行国王不能放在相同列,不能斜对角
if ((sit[j] & sit[x]) != 0) return false; // 同一列有国王,不合法
if (((sit[j] << 1) & sit[x]) != 0) return false; // 左斜对角有国王,不合法
if ((sit[j] & (sit[x] << 1)) != 0) return false; // 右斜对角有国王,不合法
return true; // 否则合法
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt(); // 读取棋盘列数(即每行有多少列)
k = scanner.nextInt(); // 读取需要放置的国王数量
scanner.close();
dfs(0, 0, 0); // 预处理所有合法的行状态,存入 sit 和 sta 数组
// 初始化 DP:第一行的状态转移
for (int j = 1; j <= cnt; j++) {
f[1][j][(int) sta[j]] = 1; // 只有一行时,状态 j 且国王数量为 sta[j],方案数为 1
}
// 计算 DP
for (int i = 2; i <= n; i++) { // 遍历行数(从第 2 行开始)
for (int j = 1; j <= cnt; j++) { // 遍历当前行的所有合法状态 j
for (int x = 1; x <= cnt; x++) { // 遍历上一行的所有合法状态 x
if (!compatible(j, x)) continue; // 如果状态 j 和 x 不兼容,则跳过
// 遍历当前状态 j 的国王数量,并进行状态转移
for (int l = (int) sta[j]; l <= k; l++) {
f[i][j][l] += f[i - 1][x][l - (int) sta[j]]; // 状态转移
}
}
}
}
// 计算最终答案:累加所有可能的最后一行状态,得到方案总数
long ans = 0;
for (int i = 1; i <= cnt; i++) {
ans += f[n][i][k];
}
System.out.println(ans); // 输出最终方案数
}
}

浙公网安备 33010602011771号