Core Logic and Mathematical Principles
Plug Dynamic Programming (Plug DP), also known as Profile Dynamic Programming, is the most dominant and ultimate form of state compression dynamic programming. It is specifically designed to solve the connectivity path loop problem on grids (for example, drawing a non-self-intersecting closed loop on the grid, or counting the number of Hamiltonian cycles).
In Section 17.2's grid compression, we performed inter-row transfers on a "whole row" basis. However, if the constraint changes to "the path must be connected and non-self-intersecting," merely recording the state of the previous row cannot reveal how the current segments are connected on a macro level. Blindly recording the connectivity network of the entire graph will lead to an exponential collapse of the state quantity.
Plug DP introduces Profiles and Plugs, perfectly transforming the global connectivity constraints into precise recursions of local dynamic programming.
1. Geometric Dimensionality Reduction: From "Row Transfer" to "Cell-by-Cell Profile Transfer"
Plug DP no longer advances row by row but cell by cell (Cell by Cell).
- Profile: A polyline that separates the "decided cells" from the "undecided cells." For a grid with $M$ columns, the profile contains $M$ vertical edges and 1 horizontal edge, totaling $M+1$ parts.
- Plug: A geometric representation of the path crossing the profile. Since the path must be continuous, each cell has 4 possible plugs (up, down, left, right). The existence of a plug (denoted as 1) indicates that the path crosses the profile and extends into the undecided area.
2. Algebraic Decoupling of Connectivity: Bracket Notation
Knowing only the positions of plugs on the profile (binary compression) is insufficient, as we cannot determine which two plugs are internally connected. If we accidentally close two paths that originally belong to the same connected component too early, it will create a "locally independent small loop" in the middle of the graph, leading to a failure in determining the global Hamiltonian cycle.
Since the paths do not intersect in the planar grid, the connectivity relationships of the plugs on the profile have a natural nesting property, which is mathematically isomorphic to ground state bracket matching:
- Left bracket
((base-1/state 1): Indicates that this is the left endpoint of a connected component (plug in). - Right bracket
)(base-2/state 2): Indicates that this is the right endpoint of a connected component (plug out). - No plug
_(base-0/state 0): Indicates that no path crosses the current edge.
This state compression based on three states (0, 1, 2) is usually mapped in memory using base-4 (Bitwise Base-4). A grid with $M$ columns has $M+1$ positions on the profile, and the state scalar is represented using bitwise operations as: State = (p_0 p_1 ... p_{M})_4.
State Design and Algorithm Derivation (Taking Hamiltonian Cycle as an Example)
As we roll from left to right and top to bottom to decide on the grid point $(i, j)$, the states of the left profile plug $L$ and upper profile plug $U$ are known. We need to classify the discussion of their bracket forms to determine the dynamic transfer mechanism for the new plugs on the right and below (classification discussion matrix):
1. Case A: $L = 0, U = 0$ (No plugs flowing in)
Since this is a Hamiltonian cycle, every cell must be traversed by the path. To survive, the current cell must create a new path extending right and down from nothing.
- Transfer Decision: In the new profile, the right and bottom sides of the current cell produce a
(and)respectively. - State Encoding: Rewrite the positions of $L$ and $U$ as $1$ (left bracket) and $2$ (right bracket).
2. Case B: One of $L$ and $U$ is 0, the other is not
This indicates that a path is flowing in from one side, giving the current cell two choices: either continue straight or turn.
- Transfer Decision: Retain the original bracket properties; the path can extend downwards or to the right.
- State Encoding: Keep the state and shift the position.
3. Case C: $L = 1, U = 1$ (Two left brackets collide)
Two left endpoints of different connected components converge at the current cell. After connecting, these two paths globally form one.
- Topological Mutation: Since the two left brackets close, the corresponding right bracket in the pair that was originally paired with $U$ must be forced to mutate into a left bracket to maintain the balance of global bracket matching.
4. Case D: $L = 2, U = 2$ (Two right brackets collide)
Symmetrically to Case C, two right brackets connect.
- Topological Mutation: The corresponding left bracket that originally paired with $L$ must be forced to mutate into a right bracket.
5. Case E: $L = 2, U = 1$ (Right bracket collides with left bracket)
This indicates that two different path segments perfectly connect in the middle.
- Topological Mutation: Directly cancel them out, converting them into the no plug state $0$. The remaining global brackets remain unchanged.
6. Case F: $L = 1, U = 2$ (Left bracket collides with right bracket)
This is the most critical endgame determination! The left bracket is on the left and the right bracket is on the right, indicating that they are the two endpoints of the same connected component. If they connect, it will completely close the connected component into a loop.
- Loop Verification: This closure is only valid if the current cell is exactly the last legal empty cell of the entire grid (indicating that the entire graph has been closed into a unique large loop); otherwise, premature closure means producing an illegal local independent small loop, and this state is directly pruned and discarded.
C++ Standard Source Code (NOIP/Provincial Selection High Precision Constant General Template)
Due to the extremely sparse valid states of Plug DP (although the upper limit is $4^{M+1}$, the legal states that satisfy bracket matching are extremely few), during the exam, it is essential to use a hash table to dynamically maintain and roll forward the state space.
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int HASH_SIZE = 4001; // Hash table capacity
const int MAX_STATES = 100005;
int n, m;
int maze[15][15];
int end_x, end_y; // Record the position of the last empty cell
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; // State already exists, accumulate the number of schemes
return;
}
}
// Insert new state
state[size] = st;
value[size] = val;
next[size] = head[key];
head[key] = size++;
}
} ht[2]; // Rolling hash table
// Helper Operator: Get the bracket state of the i-th position in base-4 state
inline int get_bit(long long st, int i) {
return (st >> (i << 1)) & 3;
}
// Helper Operator: Set the bracket state of the i-th position in base-4 state
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; // Dynamically locate the endpoint
}
}
}
long long ans = 0;
int cur = 0;
ht[cur].clear();
ht[cur].insert(0, 1); // Initial no plugs, number of schemes is 1
for (int i = 1; i <= n; ++i) {
// Critical exam detail: When switching lines, the profile must shift right by one cell
// In base-4, this is equivalent to shifting all states left by 2 bits (i.e., adding a no plug state 0 on the leftmost side)
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); // Left plug L
int p_up = get_bit(st, j); // Upper plug U
if (!maze[i][j]) {
// If it is an obstacle cell, no plugs can flow in or out
if (p_left == 0 && p_up == 0) {
ht[nxt].insert(st, val);
}
continue;
}
// Classify discussion of state machine matrix
if (p_left == 0 && p_up == 0) {
// Case A: Add a new pair of brackets
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) {
// Case B: Single plug continuation
int p_val = p_left ? p_left : p_up;
if (maze[i][j + 1]) { // Extend right
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]) { // Extend down
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) {
// Case C: Two left brackets collide, change right bracket to left
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) {
// Case D: Two right brackets collide, change left bracket to right
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) {
// Case E: Right and left connect, naturally cancel out
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) {
// Case F: Endgame loop closure
if (i == end_x && j == end_y) {
// Verify that the remaining plugs are all 0 to ensure it is a unique large loop
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/Provincial Selection Practical Pitfall Guide
1. End of Row Not Executing Line Shift Status (Shift Operational Bug)
- Phenomenon: When a row's decision ends and prepares to enter the first cell of the next row, if the shift operation is not performed, the vertical plug at the far right of the previous row will directly misalign into the left plug of the next row. This geometrically equates to the path directly penetrating the physical outer boundary of the grid, causing the answer to always be 0 or outputting a wide range of dirty data.
- Avoidance Method: Remember the line switching control line: Before entering the column loop, the current hash table's state scalars must be forcibly shifted left by 2 bits (i.e.,
st <<= 2).
2. High Bit of Bitwise Operation Not Adding LL Suffix Leading to Truncation
- Phenomenon: When refreshing the base-4 state through
set_bit,3 << (i << 1)is used. If the number of grid columns $M = 12$,i << 1reaches $24$, and the bitwise operation, although not overflowing in the defaultint(32 bits), will cause 32-bit integer high bit truncation when $M$ is larger or involves longer profiles, leading to a large area collapse of the state. - Avoidance Method: For high-bit state compression shift masks, always append a large integer suffix:
3LL << (i << 1)or(long long)v << (i << 1).
Classic Provincial Selection/NOI Real Questions
1. Luogu P2289 [HNOI2004] Postman
- Problem Description: Given an $N \times M$ grid, find the total number of closed loops (Hamiltonian cycles) that pass through all vertices exactly once. $N \le 20, M \le 10$.
- Essence of the Problem and Solution Idea: A classic unobstructed complete Hamiltonian cycle problem. Directly apply the plug DP bracket notation template above. Since the final number of schemes for this problem is extremely large and will exceed the limit of
long long, an additional high-precision integer class (__int128or manual high-precision array) needs to be externally connected to replace thevaluestorage in the hash table to directly achieve AC and score full marks.
2. Luogu P3190 [HNOI2007] Magical Amusement Park
- Problem Description: Given an $N \times M$ grid, each cell has a weight (which can be positive or negative). The goal is to draw a legal single loop (not necessarily passing through all cells) in the grid such that the sum of the weights of the cells contained in the loop is maximized. $N \le 100, M \le 6$.
- Essence of the Problem and Solution Idea: Maximum weight single loop plug DP.
- Core Difficulty: It is not necessary to pass through all cells. This means that each cell can not only be forced to produce plugs but also choose "not to draw into the path at all."
- State Correction: The
valuein the hash table no longer stores the number of schemes but instead stores the maximum weight sum. In the classification discussion, an additional branch of "skipping the current cell" is added: when $L=0, U=0$, not only can new brackets be generated, but it can also choose to keep $L=0, U=0$ and directly transfer to the next cell without adding the current cell's weight. The final answer is obtained by performing a global maximum search at all valid moments triggered by Case F (premature closure of loops).