状压DP

一、什么是状态压缩动态规划(状压DP)

状态压缩动态规划(简称状压DP)是一种利用位运算高效表示和处理状态的动态规划方法。它通过将多维状态压缩到整数二进制位中,将原本难以处理的状态转化为位操作问题。

适用场景特征:

  1. 状态维度高但每维取值范围小(通常为布尔型)
  2. 状态之间存在可转移性
  3. 需要高效处理状态间关系

二、核心原理与位运算基础

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;
}

五、优化技巧与注意事项

  1. 滚动数组优化:当当前状态只依赖前一状态时,可用两个数组交替使用
vector<long long> prev(dp[0]);
vector<long long> curr(dp[0].size());
// 每轮交换prev和curr
  1. 按状态特征分组:根据状态中1的个数进行阶段转移
for (int cnt = 0; cnt <= n; ++cnt) {
    for (auto mask : states_with_cnt[cnt]) {
        // 处理该阶段的状态转移
    }
}
  1. 预处理合法转移:预先计算每个状态的可能转移目标

  2. 剪枝优化:在循环中及时跳过不可能的状态

六、常见问题与解决方案

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);  // 输出最终方案数
    }
}
posted @ 2025-03-02 17:02  咋还没来  阅读(214)  评论(0)    收藏  举报