📘状压DP · 状态压缩动态规划

2026-08-09
⭐⭐⭐⭐ GESP 8级

📖概念讲解

状压DP(状态压缩动态规划)是用二进制整数来表示集合状态的DP技巧。把"哪些元素被选中"压缩成一个整数的二进制位——第 i 位为 1 表示第 i 个元素在集合中。

核心思想:n 个元素的所有子集最多 2ⁿ 个,用 0 ~ 2ⁿ−1 的整数就能枚举。适合 n ≤ 20 的场景。

🔑 三个必备操作:
• 判断第 i 位: mask & (1 << i) — 非零则 i 在集合中
• 加入第 i 位: mask | (1 << i)
• 去掉第 i 位: mask & ~(1 << i)

经典问题:TSP(旅行商问题)—— n 个城市,每个城市恰好访问一次,求最短回路。状态 dp[S][i] 表示已访问城市集合为 S、当前在城市 i 的最短路径。

💻代码示例

下面用状压DP解 TSP 问题(n ≤ 15):

1#include <iostream>
2#include <cstring>
3#include <algorithm>
4using namespace std;
5const int N = 15, INF = 0x3f3f3f3f;
6int dist[N][N]; // 邻接矩阵存距离
7int dp[1 << N][N]; // dp[S][i]: 集合S且在城市i的最短距离
8int n;
9
10int main() {
11 cin >> n;
12 for(int i = 0; i < n; i++)
13 for(int j = 0; j < n; j++)
14 cin >> dist[i][j];
15
16 // 初始化:只访问过城市0,当前在城市0,距离为0
17 memset(dp, 0x3f, sizeof(dp)); // 全部设为INF
18 dp[1][0] = 0; // 000...001 表示只有城市0
19
20 // 枚举所有集合 S
21 for(int S = 0; S < (1 << n); S++) {
22 for(int i = 0; i < n; i++) { // i = 当前所在城市
23 if(dp[S][i] == INF) continue; // 不可达则跳过
24 for(int j = 0; j < n; j++) { // j = 下一个要去的城市
25 if(S & (1 << j)) continue; // j已经在集合S中,跳过
26 int ns = S | (1 << j); // 新集合:加入城市j
27 dp[ns][j] = min(dp[ns][j], // 更新最短距离
28 dp[S][i] + dist[i][j]);
29 }
30 }
31 }
32
33 // 回到起点0:所有城市都访问过(全1)且在城市i,再走回0
34 int full = (1 << n) - 1; // 全1 = 111...111
35 int ans = INF;
36 for(int i = 0; i < n; i++)
37 ans = min(ans, dp[full][i] + dist[i][0]); // 从i走回起点
38
39 cout << ans << endl;
40 return 0;
41}
⏱ 复杂度:O(2ⁿ × n²),n=15 时约 5×10⁶,完全可接受。

🧩互动小测

Q1:n=5 时,全集 mask 的值是多少?

Q2:如何检查城市 3 是否在集合 S 中?

Q3:状压DP的时间复杂度是?

🏋️动手练一练

📝 编程练习:最短汉密尔顿路径

给定 n 个点的带权有向图(邻接矩阵),求从点 0 出发、经过所有点恰好一次的最短路径长度。

输入格式:第一行 n(2 ≤ n ≤ 15),接下来 n 行每行 n 个整数表示距离矩阵。
输出:一个整数,最短路径长度。

提示:和例题几乎一样,只是不用回到起点。状态定义相同:dp[S][i] 表示走过集合 S 中的城市、当前在城市 i 的最短距离。
参考答案:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=15,INF=0x3f3f3f3f;
int dist[N][N],dp[1<<N][N],n;
int main(){
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++) cin>>dist[i][j];
    memset(dp,0x3f,sizeof(dp));
    dp[1][0]=0; // 起点在城市0
    for(int S=0;S<(1<<n);S++)
        for(int i=0;i<n;i++){
            if(dp[S][i]==INF) continue;
            for(int j=0;j<n;j++){
                if(S&(1<<j)) continue;
                int ns=S|(1<<j);
                dp[ns][j]=min(dp[ns][j],dp[S][i]+dist[i][j]);
            }
        }
    int full=(1<<n)-1,ans=INF;
    for(int i=0;i<n;i++)
        ans=min(ans,dp[full][i]); // 不用回到起点
    cout<<ans;
}

要点:区别只在最终答案——取所有 dp[full][i] 的最小值,不需要再加 dist[i][0] 回到起点。

📝易错点提醒

学完这个知识点后点一下