【动态规划】状压DP

🧠 一、什么是状压 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 位设为 1state | (1 << i)选中第 i 个元素
将第 i 位设为 0state & ~(1 << i)取消选中第 i 个元素
判断是否包含某个子集 s(state & s) == ssstate 的子集
枚举子集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 的个数

🧪 七、调试技巧

  • 打印状态转移过程,观察 maski 的变化。
  • 使用小样例(如 n=3)手动模拟状态转移。
  • 注意边界条件:初始状态、非法状态跳过、最终状态判断。

✅ 八、总结

要点内容
核心思想用二进制整数表示集合状态
关键技巧位运算操作(取位、置位、子集枚举)
状态设计dp[mask][...],mask 表示已处理集合

| 典型问题 | TSP、棋盘覆盖、集合划分、Hamilton 路径 |


📌 九、常见错误与注意事项

  1. 数组大小不够dp[1<<n][n],注意 1<<n2^n,不要写成 1<<n 在栈上定义过大。
  2. 未初始化为 INF:导致状态被错误更新。
  3. 忽略状态合法性:如 mask 不包含 i 却访问 dp[mask][i]
  4. 边界处理错误:如 TSP 忘记返回起点。
  5. 位运算优先级>>& 优先级较低,记得加括号:(mask >> i) & 1

🧰 十、扩展阅读

  • 轮廓线 DP(插头 DP):更复杂的状压形式,用于棋盘路径问题。
  • 快速子集卷积:用于集合卷积类问题,高级技巧。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值