状态压缩 DPCSP-S

目录

💡 核心思想

状态压缩 DP 用二进制整数来表示集合状态,每一位 0/1 代表一个元素是否被选中。当元素数量较少(通常 $\le 20$)时,可以用一个整数编码所有可能的子集,实现紧凑的状态表示。

状压 DP 的核心技巧是位运算:$S | (1 << i)$ 表示集合 $S$ 加入元素 $i$,$S \& (1 << i)$ 判断元素 $i$ 是否在 $S$ 中。这类题目的标志是"数据范围小($n \le 20$),但组合爆炸"。TSP 问题是最经典的状压 DP。

🎯 直觉理解

想象你有 5 个任务要安排顺序。用 5 位二进制数表示哪些任务已完成:00000 = 都没做,10110 = 第2、3、5个做完了。这样所有可能的完成情况恰好是 $2^5 = 32$ 种,用一个整数就能表示。

📝 算法流程

  1. 状态:$dp[S]$ 表示已完成集合 $S$ 时的最优值
  2. 转移:枚举 $S$ 中没有的元素 $i$,从 $S - \{i\}$ 转移
  3. 遍历:按集合大小从小到大
  4. 总状态数:$2^n$

$$dp[S] = \min_{i \in S} \{dp[S \setminus \{i\}] + cost(i, S)\}$$

📊 复杂度分析

指标复杂度
时间$O(2^n \cdot n)$
空间$O(2^n)$

💻 参考实现(C++)

C++ (C++17)
#include <bits/stdc++.h>
using namespace std;
// TSP 问题:从城市0出发经过所有城市后返回
int main() {
    int n; cin >> n;
    vector dist(n, vector<int>(n));
    for (auto& r : dist) for (auto& x : r) cin >> x;
    const int INF = 1e9;
    vector<vector<int>> dp(1<<n, vector<int>(n, INF));
    dp[1][0] = 0; // 只访问了城市0
    for (int S = 1; S < (1<<n); S++) {
        for (int u = 0; u < n; u++) {
            if (!(S >> u & 1)) continue;
            for (int v = 0; v < n; v++) {
                if (S >> v & 1) continue;
                dp[S|(1<<v)][v] = min(dp[S|(1<<v)][v], dp[S][u] + dist[u][v]);
            }
        }
    }
    int ans = INF;
    for (int i = 1; i < n; i++)
        ans = min(ans, dp[(1<<n)-1][i] + dist[i][0]);
    cout << ans << endl;
    return 0;
}

⚠️ 常见坑点

状态数 $2^n$ 很大,$n > 20$ 通常不可行

位运算优先级错误——多加括号

忘记初始化 dp 数组为 INF

枚举顺序要确保转移来源已经算好

📚 相关题目

题目来源难度备注
P1896 互不侵犯洛谷CSP-S棋盘状压DP
P2704 NOI2001 炮兵阵地洛谷CSP-S经典状压DP