NeFut Logo NeFut
EN 管理员登录

插头动态规划:解析棋盘格连通性路径闭环的极致算法

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #Dynamic Programming #DP

核心逻辑与数学原理

插头动态规划(Plug DP),又称轮廓线动态规划(Profile DP),是状态压缩动态规划中最具统治力的终极形态。它专门用于解决棋盘格上的连通性路径闭环问题(例如:在网格中画出一条不自交的闭合回路,或求解哈密顿回路的数量)。

在 17.2 节的棋盘状压中,我们以“整行”为单位进行跨行转移。但如果约束条件变为“路径必须连通且不自交”,仅记录上一行的状态根本无法得知当前的线段在宏观上是如何连接的。如果盲目记录整张图的连通网络,状态量将会发生指数级坍塌。

插头 DP 通过引入轮廓线(Profile)插头(Plug),将全局的连通性约束完美转化为局部动态机的精确递推。

1. 几何降维:从“行转移”到“逐格轮廓线转移”

插头 DP 不再逐行推进,而是逐格(Cell by Cell)推进。

2. 连通性的代数解耦:括号表示法(Bracket Notation)

仅仅知道轮廓线上哪些位置有插头(二进制状压)是不够的,因为我们无法判断哪两个插头在内部是相连的。如果不小心将两条原本属于同一个连通块的路径提早闭合,就会在图中间暴毙出一个“局部独立小回路”,导致全局哈密顿回路判定失败。

由于路径在平面网格内互不自交,轮廓线上的插头连通关系具有天然的嵌套性,这在数学上与基态括号匹配完全同构:

这种基于三种状态(0, 1, 2)的状态压缩,我们通常采用四进制(Bitwise Base-4)进行内存映射。一个拥有 $M$ 列的棋盘,轮廓线上有 $M+1$ 个位置,状态标量通过位运算表示为:State = (p_0 p_1 ... p_{M})_4


状态设计与算法推导(以哈密顿回路为例)

当我们从左到右、从上到下滚动决策到格点 $(i, j)$ 时,当前格子的左侧轮廓线插头 $L$上方轮廓线插头 $U$ 的状态是已知的。我们需要根据 $L$ 和 $U$ 的括号形态,分类讨论其对右侧和下方新插头的动态转移机(分类讨论矩阵):

1. 情况 A:$L = 0, U = 0$(无插头流入)

由于是哈密顿回路,每个格子必须被路径流经。当前格子为了活命,必须自己凭空制造一个向右和向下的新路径。

2. 情况 B:$L$ 和 $U$ 中有一个为 0,另一个不为 0

说明有一条路径从一侧流入,它在当前格子有两项选择:要么直行,要么拐弯。

3. 情况 C:$L = 1, U = 1$(两个左括号相撞)

两条属于不同连通块的左端点路径在当前格子汇合。它们对接后,这两条路径在全局上连成了一条。

4. 情况 D:$L = 2, U = 2$(两个右括号相撞)

与情况 C 对称,两个右括号对接。

5. 情况 E:$L = 2, U = 1$(右括号与左括号相撞)

这代表两条不同的路径片段在中间完美接头。

6. 情况 F:$L = 1, U = 2$(左括号与右括号相撞)

这是最致命的终局判定!左括号在左,右括号在右,说明它们属于同一个连通块的两个端点。如果把它们对接,就会让这个连通块彻底闭合形成回路。


C++ 标准源码(NOIP/省选高精常数通用模板)

由于插头 DP 的有效状态极为稀疏(虽然空间上限是 $4^{M+1}$,但满足括号匹配的合法状态极少),在考场上必须使用哈希表(Hash Table)来动态维护和滚动递推状态空间

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int HASH_SIZE = 4001; // 哈希表容量
const int MAX_STATES = 100005;

int n, m;
int maze[15][15];
int end_x, end_y; // 记录最后一个空格的位置

struct HashTable {
    int head[HASH_SIZE], next[MAX_STATES], size;
    long long state[MAX_STATES], value[MAX_STATES];

    void clear() {
        size = 0;
        memset(head, -1, sizeof(head));
    }

    void insert(long long st, long long val) {
        int key = st % HASH_SIZE;
        for (int i = head[key]; i != -1; i = next[i]) {
            if (state[i] == st) {
                value[i] += val; // 状态已存在,累加方案数
                return;
            }
        }
        // 插入新状态
        state[size] = st;
        value[size] = val;
        next[size] = head[key];
        head[key] = size++;
    }
} ht[2]; // 滚动哈希表

// 辅助算子:获取四进制状态下第 i 个位置的括号状态
inline int get_bit(long long st, int i) {
    return (st >> (i << 1)) & 3;
}

// 辅助算子:设置四进制状态下第 i 个位置的括号状态
inline long long set_bit(long long st, int i, int v) {
    return (st & ~(3LL << (i << 1))) | ((long long)v << (i << 1));
}

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

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

    end_x = 0; end_y = 0;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            char ch; cin >> ch;
            if (ch == '.') {
                maze[i][j] = 1;
                end_x = i; end_y = j; // 动态定位终点
            }
        }
    }

    long long ans = 0;
    int cur = 0;
    ht[cur].clear();
    ht[cur].insert(0, 1); // 初始无插头,方案数为 1

    for (int i = 1; i <= n; ++i) {
        // 致命考场细节:换行时,轮廓线整体需要向右平移一格
        // 在四进制下等价于将所有状态统一左移 2 位(即在最左边补一个无插头状态 0)
        for (int k = 0; k < ht[cur].size; ++k) {
            ht[cur].state[k] <<= 2;
        }

        for (int j = 1; j <= m; ++j) {
            int nxt = cur ^ 1;
            ht[nxt].clear();

            for (int k = 0; k < ht[cur].size; ++k) {
                long long st = ht[cur].state[k];
                long long val = ht[cur].value[k];

                int p_left = get_bit(st, j - 1); // 左插头 L
                int p_up = get_bit(st, j);       // 上插头 U

                if (!maze[i][j]) {
                    // 如果是障碍物格子,不能有任何插头流入或流出
                    if (p_left == 0 && p_up == 0) {
                        ht[nxt].insert(st, val);
                    }
                    continue;
                }

                // 分类讨论状态机矩阵
                if (p_left == 0 && p_up == 0) {
                    // 情况 A:添加一对新括号
                    if (maze[i][j + 1] && maze[i + 1][j]) {
                        long long new_st = set_bit(st, j - 1, 1);
                        new_st = set_bit(new_st, j, 2);
                        ht[nxt].insert(new_st, val);
                    }
                }
                else if (p_left == 0 || p_up == 0) {
                    // 情况 B:单插头延续
                    int p_val = p_left ? p_left : p_up;
                    if (maze[i][j + 1]) { // 向右延伸
                        long long new_st = set_bit(st, j - 1, 0);
                        new_st = set_bit(new_st, j, p_val);
                        ht[nxt].insert(new_st, val);
                    }
                    if (maze[i + 1][j]) { // 向下延伸
                        long long new_st = set_bit(st, j - 1, p_val);
                        new_st = set_bit(new_st, j, 0);
                        ht[nxt].insert(new_st, val);
                    }
                }
                else if (p_left == 1 && p_up == 1) {
                    // 情况 C:两个左括号相撞,改右括号为左
                    int count = 1;
                    for (int l = j + 1; l <= m; ++l) {
                        if (get_bit(st, l) == 1) count++;
                        if (get_bit(st, l) == 2) count--;
                        if (count == 0) {
                            long long new_st = set_bit(st, l, 1);
                            new_st = set_bit(new_st, j - 1, 0);
                            new_st = set_bit(new_st, j, 0);
                            ht[nxt].insert(new_st, val);
                            break;
                        }
                    }
                }
                else if (p_left == 2 && p_up == 2) {
                    // 情况 D:两个右括号相撞,改左括号为右
                    int count = 1;
                    for (int l = j - 2; l >= 0; --l) {
                        if (get_bit(st, l) == 2) count++;
                        if (get_bit(st, l) == 1) count--;
                        if (count == 0) {
                            long long new_st = set_bit(st, l, 2);
                            new_st = set_bit(new_st, j - 1, 0);
                            new_st = set_bit(new_st, j, 0);
                            ht[nxt].insert(new_st, val);
                            break;
                        }
                    }
                }
                else if (p_left == 2 && p_up == 1) {
                    // 情况 E:右左对接,自然消亡
                    long long new_st = set_bit(st, j - 1, 0);
                    new_st = set_bit(new_st, j, 0);
                    ht[nxt].insert(new_st, val);
                }
                else if (p_left == 1 && p_up == 2) {
                    // 情况 F:终局回路闭合
                    if (i == end_x && j == end_y) {
                        // 校验剩余插头全为 0 确保其为唯一大回路
                        long long new_st = set_bit(st, j - 1, 0);
                        new_st = set_bit(new_st, j, 0);
                        if (new_st == 0) ans += val;
                    }
                }
            }
            cur = nxt;
        }
    }

    cout << ans << "\n";
    return 0;
}

NOIP/省选 实战避坑指南

1. 行末未执行换行状态移位(Shift Operational Bug)

2. 位运算高位未加 LL 后缀导致截断


经典省选/NOI 真题

1. 洛谷 P2289 [HNOI2004] 邮递员

2. 洛谷 P3190 [HNOI2007] 神奇游乐园

原文链接: local://17.3

[h] 返回首页