NeFut Logo NeFut
EN 管理员登录

棋盘状态压缩动态规划:无后效性与位运算的深度解析

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#C++ #Tutorial

核心逻辑与数学原理

棋盘式状态压缩动态规划(Grid-based State Compression DP)的核心本质是利用空间局部相依性,将高维的二维网格拓扑约束,降维映射为一维行(或列)状态向量之间的状态机交织

在 17.1 节中,我们探讨了以“顶点集合”为压缩尺度的点集状压。而在处理棋盘格问题(如国王放置、互不侵犯等问题)时,约束条件往往只存在于相邻行与相邻列之间。

1. 空间无后效性的物理隔离

假定一个 $N \times M$ 的网格,我们在放置棋子时,若按照“自上而下,逐行决策”的拓扑序推进,那么当我们在对第 $i$ 行进行棋子放置时:

因此,我们不需要记录整个棋盘的宏观面貌,只需要将“当前行”的棋子放置形态压缩为一个二进制掩码 $S$,即可完美满足无后效性

2. 位运算冲突校验矩阵

假设在一行 $M$ 列的网格中,用二进制状态 $S$ 表示该行棋子的放置情况(1表示放,0表示空)。


状态设计与算法推导

以经典的“互不侵犯(King)”问题为例:在 $N \times N$ 的棋盘格中放置 $K$ 个国王,国王能攻击其周围八个方向相邻的格子。求互不侵犯地放置 $K$ 个国王的合法方案总数。

1. 状态空间定义

设状态 $f[i][j][S]$ 表示:已经决策完前 $i$ 行,且整个棋盘上已经放置了 $j$ 个国王,且当前第 $i$ 行的国王放置形态掩码为 $S$ 时的合法方案数。

2. 状态转移方程

对于当前行状态 $S$,首先必须保证其行内合法。接着,我们需要通过 __builtin_popcount(S) 算子获取当前行贡献的国王数量,记为 $c$。

$$\forall S_{pre} \text{ compatible with } S, \quad f[i][j][S] = \sum f[i-1][j - c][S_{pre}]$$

其中“$S_{pre} \text{ compatible with } S$”需严格通过上文所述的三项跨行位运算校验。


C++ 标准源码(NOIP风格)

以下源码为“互不侵犯/国王放置”的工业级标准实现。代码中加入了合法状态空间预处理优化,可大幅榨干常数,确保在 Linux g++ -O2 下实现 0.00s 瞬杀。

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

const int MAXN = 10;
const int MAXK = 100;

long long f[MAXN][MAXK][1 << MAXN];
vector<int> valid_states; // 预处理存储所有行内合法的状态,避免无效枚举
int state_weights[1 << MAXN]; // 预处理每个二进制状态中 1 的个数(即国王数)

int n, k_total;

// 预处理函数:剔除行内本身就左右冲突的状态,并计算权值
void preprocess() {
    int max_state = 1 << n;
    for (int s = 0; s < max_state; ++s) {
        // 水平方向无相邻国王
        if (!(s & (s << 1))) {
            valid_states.push_back(s);
            state_weights[s] = __builtin_popcount(s); // 快速计算二进制中 1 的个数
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    if (!(cin >> n >> k_total)) return 0;

    preprocess();

    // 边界初始化:第 0 行没有任何棋子,国王数为 0,形态为 0 的方案数为 1
    f[0][0][0] = 1;

    // 拓扑轴:逐行推进
    for (int i = 1; i <= n; ++i) {
        // 枚举当前行状态 S(必须是预处理合法的)
        for (int s : valid_states) {
            int c = state_weights[s]; // 当前行放置的国王数

            // 枚举上一行状态 S_pre(同样来自合法集)
            for (int s_pre : valid_states) {
                // 跨行冲突全面校验:正上下、左下斜、右下斜
                if (s & s_pre) continue;
                if (s & (s_pre << 1)) continue;
                if (s & (s_pre >> 1)) continue;

                // 枚举截止到上一行为止,已经放置的国王总数 j
                for (int j = c; j <= k_total; ++j) {
                    f[i][j][s] += f[i - 1][j - c][s_pre];
                }
            }
        }
    }

    // 全局收益收集:将最后一行所有可能状态的方案数横向累加
    long long ans = 0;
    for (int s : valid_states) {
        ans += f[n][k_total][s];
    }

    cout << ans << "\n";

    return 0;
}

NOIP 实战避坑指南

1. 盲目暴力枚举引发时间复杂度隐式爆炸

2. 位移边界溢出溢损


经典 NOIP/洛谷 真题

1. 洛谷 P1896 [SCOI2005] 互不侵犯

2. 洛谷 P2704 [NOI2001] 炮兵阵地

$$f[i][S][S_{pre}] = \max \{ f[i-1][S_{pre Visited}][S_{pre\_pre}] \} + \text{count}(S)$$

在校验时,除了执行行内隔 2 格校验 (S & (S << 1)) == 0 && (S & (S << 2)) == 0 外,还要同时满足 (S & S_pre) == 0 以及 (S & S_pre_pre) == 0。通过多加一维状态成功解耦了二阶空间后效性,是棋盘状压的终极进阶必刷题。

原文链接: local://17.2

[h] 返回首页