插头动态规划 (Plug Dynamic Programming)
插头 DP(又称轮廓线 DP)是状压 DP 的一种高级形态,专门用于解决棋盘或网格图上的连通性问题(如路径计数、哈密顿回路、连通块覆盖等)。其核心在于维护一条跨越网格的轮廓线,并记录穿过该线的连通状态。
1. 状态表示:括号序列与最小表示法
由于连通性具有“两两配对且不相交”的拓扑性质,状态表示是插头 DP 的精髓。
1.1 括号序列表示法 (Bracket Notation)
适用于路径配对问题。
0:无插头。1:左括号插头(连通分量的左端点)。2:右括号插头(连通分量的右端点)。- 性质:轮廓线上的插头构成一个合法的括号匹配序列。
1.2 最小表示法 (Minimal Representation)
适用于一般的连通块问题。
将属于同一连通块的插头标记为相同的整数编号(如 (1, 1, 2, 2, 0)),并确保编号是按出现顺序标准化后的结果。
2. 状态转移:基于格子的决策
插头 DP 逐格推进(Row by row, Column by column)。对于当前格子 ,我们关注其左侧插头 和上方插头 。
2.1 典型分类讨论 (以哈密顿回路为例)
- :必须新建一个连通分量,产生两个新插头(左括号和右括号)。
- 或 :延续当前插头,可以选择向下或向右。
- :两个左括号相遇,合并两个连通分量,并将对应的右括号改为左括号。
- :两个右括号相遇,合并并将对应的左括号改为右括号。
- :左、右括号相遇,直接闭合。
- :匹配的左、右括号相遇,若为哈密顿回路且为最后一个格子,则形成合法解。
3. 高效实现:哈希表与位运算
由于合法状态数远少于 ,必须使用哈希表存储状态。
struct HashMap {
int head[SIZE], next[STATE_MAX], total;
ll state[STATE_MAX], val[STATE_MAX];
void insert(ll s, ll v) {
int h = s % SIZE;
for (int i = head[h]; i; i = next[i])
if (state[i] == s) { val[i] += v; return; }
state[++total] = s; val[total] = v;
next[total] = head[h]; head[h] = total;
}
};
4. 综合练习:骨牌铺满 (Domino Tiling)
虽然该题可用简单轮廓线 DP,但作为插头 DP 的入门非常合适。
Check Solution (Bracket Notation Base)
// 简化的插头 DP 状态转移
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cur ^= 1; total[cur] = 0; memset(head, 0, sizeof head);
for (int k = 1; k <= total[cur ^ 1]; k++) {
ll s = state[cur ^ 1][k], v = val[cur ^ 1][k];
int L = (s >> (2 * (j - 1))) & 3;
int U = (s >> (2 * j)) & 3;
if (!L && !U) { // 必须放置,向下或向右
if (i < n && j < m) insert(s | (1 << (2*(j-1))) | (2 << (2*j)), v);
}
// ... 更多 case ...
}
}
}
延伸挑战
- 洛谷 P5056 【模板】插头 DP (哈密顿回路)
- 洛谷 P3190 [HNOI2007] 神奇游乐园 (最大权重路径)
- HDU 1693 Eat the Trees (简单回路计数)
- Codeforces 8C Looking for Order (状压与插头思想结合)