Core Logic and Mathematical Principles
The essence of search is traversing the state space tree. Facing an exponentially growing state space, the underlying logic of pruning is to terminate invalid branches early, reducing the theoretical upper bound of search complexity from $O(k^N)$ to a practically executable scale.
- Feasibility Pruning: Check constraints at the current node. Once it is found that the current path can no longer satisfy legality (e.g., out of bounds, resources exhausted), backtrack immediately. Its mathematical expression is: let the current state be $S$ and the constraint function be $g(S)$; if $g(S) = \text{False}$, then cut off the entire subtree rooted at $S$.
- Optimality Pruning: Maintain a global optimal solution
ans. During the search, if the current cost $val(S)$ plus the subsequent estimated minimum cost $h(S)$ is already greater than or equal toans, then this branch cannot yield a better solution and should be terminated. The mathematical decision condition is: $val(S) + h(S) \ge \text{ans}$. - Bound Pruning: For permutation or combination searches, accurately compute the valid value interval $[L, R]$ for the variable $x_i$ at the current level through preprocessing or mathematical derivation. By strictly limiting the enumeration boundaries of the
forloop, invalid branch generation is eliminated.
State Space Tree
What is a State Space Tree?
A state space tree is a logical and mathematical abstract model.
When solving a problem, all possible "moves" and "states" are drawn as a large tree in chronological order. Through this tree, we can transform an abstract search problem into a concrete maze game of "finding the endpoint on a tree."
Core Components
- Root Node: The initial state of the problem. For example, "haven't started making the cake" in the cake problem, or "standing at the starting point" in a maze problem.
- Branches/Edges: Each of your decisions or operations. For example, choosing radius $R=5$ at the current level, or taking one step east in a maze.
- Internal Nodes: Intermediate states reached after making decisions. It records the "resources" you have accumulated at this point (such as current cumulative volume, surface area, coordinates, etc.).
- Leaf Nodes: The endpoints of the search. There are two possibilities: one is a valid solution that reaches the boundary (successfully made the cake), and the other is a dead end with no further moves.
Relationship Between Algorithms and State Space Trees
All search and optimization algorithms essentially operate on this virtual tree:
- DFS (Depth-First Search): Like a stubborn person who grabs one branch and goes all the way down until hitting a leaf node, then returns to the previous fork (backtracking).
- BFS (Breadth-First Search): Like water spreading sideways, first completing all branches at the first level, then advancing to the second level simultaneously.
- Pruning: When going down a branch, at some intermediate node you discover "continuing down will definitely exceed weight" or "continuing down cannot be better than the solution already found," you prune to avoid wasted effort.
State Space Graphs and State Space Trees
The essence of state space is often a graph (possibly with cycles), but when the search algorithm's logical tentacles reach out, it always views it as a tree. Writing pruning and deduplication logic is essentially "shaving" this graph full of cycles, forcibly turning it into a clean, finite tree.
State Design and Algorithm Derivation
Taking the classic problem "Birthday Cake" (NOI1999 / Luogu P1731) as an example. The problem requires building an $M$-layer cake with a total volume of $N$ using cylinders, where both the radius $R$ and height $H$ of each layer are integers and strictly decreasing from bottom to top, minimizing the surface area.
1. State Design
The search state is determined by the current layer number, current volume, current surface area, and the radius and height of the previous layer. Define the DFS function state: dfs(dep, v, s, last_r, last_h), where dep is the current layer being searched (searching from bottom to top, with the bottom as layer $M$ and the top as layer $1$).
2. Derivation of Upper and Lower Bounds
For the $i$-th layer, the volume and height must be greater than or equal to the sum of the minimum volume and height of all layers above it.
- Lower bound: $R_i \ge i$, $H_i \ge i$.
- Upper bound: Determined by the remaining volume and height constraints, with boundary cases where height is 1 or radius is 1.
$$R_i \le \min\left(\lfloor \sqrt{N - v} \rfloor, \text{last\_r} - 1\right)$$
$$H_i \le \min\left(\lfloor \frac{N - v}{R_i^2} \rfloor, \text{last\_h} - 1\right)$$
3. Derivation of Pruning Strategies
- Feasibility Pruning: Preprocess the minimum volume $minV[i]$ and minimum lateral surface area $minS[i]$ from layer $1$ to layer $i$. If $v + minV[dep] > N$, backtrack immediately.
- Optimality Pruning 1: If the current surface area plus the minimum lateral surface area of the top layers already exceeds the global optimum, i.e., $s + minS[dep] \ge \text{ans}$, backtrack.
- Optimality Pruning 2 (Mathematical Scaling): The remaining volume $N - v = \sum_{j=1}^{dep} R_j^2 H_j$. The remaining lateral surface area satisfies:
$$\sum_{j=1}^{dep} 2 R_j H_j = 2 \sum_{j=1}^{dep} \frac{R_j^2 H_j}{R_j} > \frac{2}{\text{last\_r}} \sum_{j=1}^{dep} R_j^2 H_j = \frac{2(N - v)}{\text{last\_r}}$$
(Establishes the relationship between remaining lateral surface area and remaining volume)
This yields the powerful optimality pruning inequality:
$$s + \frac{2(N - v)}{\text{last\_r}} \ge \text{ans} \implies \text{backtrack}$$
Feasibility pruning prevents "remaining volume being too small," while Optimality Pruning 2 prevents "remaining volume being too large." Optimality Pruning 1 prevents "the currently accumulated surface area is too large" (prevents "the current area has already exhausted the mandatory minimum overhead of future layers").
If Optimality Pruning 2 is "dynamic feasibility pruning based on volume resources," then Optimality Pruning 1 is "dynamic feasibility pruning based on layer structure."
Minimum Volume
- Minimum volume of layer 1: $1^2 \times 1 = 1^3$
- Minimum volume of layer 2: $2^2 \times 2 = 2^3$
- Minimum volume of layer 3: $3^2 \times 3 = 3^3$
To complete the remaining dep layers of the cake, the minimum volume required in the extreme case is:
$$minV[dep] = \sum_{j=1}^{dep} j^3$$
Minimum Surface Area
- Minimum lateral surface area of layer 1: $2 \times 1 \times 1 = 2 \times 1^2$
- Minimum lateral surface area of layer 2: $2 \times 2 \times 2 = 2 \times 2^2$
- Minimum lateral surface area of layer 3: $2 \times 3 \times 3 = 2 \times 3^2$
The sum of the minimum lateral surface areas for the remaining dep layers is:
$$minS[dep] = \sum_{j=1}^{dep} 2j^2$$
C++ Core Source Code
// State: dep(current layer), v(accumulated volume), s(accumulated surface area), r(previous layer radius), h(previous layer height)
void dfs(int dep, int v, int s, int r, int h) {
if (dep == 0) {
if (v == N) ans = std::min(ans, s);
return;
}
// Feasibility pruning: current volume + minimum volume of remaining layers > target volume
if (v + min_v[dep] > N) return;
// Optimality pruning 1: current surface area + minimum lateral surface area of remaining layers >= known optimal solution
if (s + min_s[dep] >= ans) return;
// Optimality pruning 2: use mathematical inequality to estimate lower bound of future cost
if (s + 2 * (N - v) / r >= ans) return;
// Bound pruning: enumerate from bottom to top, prioritize larger sizes (optimize search order)
int max_r = std::min(static_cast<int>(std::sqrt(N - v)), r - 1);
for (int cur_r = max_r; cur_r >= dep; --cur_r) {
// Avoid state pollution in the loop by using local variables for the bottom surface contribution
int current_s = s + (dep == M ? cur_r * cur_r : 0);
int max_h = std::min((N - v) / (cur_r * cur_r), h - 1);
for (int cur_h = max_h; cur_h >= dep; --cur_h) {
dfs(dep - 1, v + cur_r * cur_r * cur_h, current_s + 2 * cur_r * cur_h, cur_r, cur_h);
}
}
}
// Start from layer M (M is the total number of layers), current volume is 0, current surface area is 0,
// the virtual (M+1)-th layer has radius and height set to a sufficiently large boundary value N
dfs(M, 0, 0, N, N);
The essence of depth-first search optimization is to establish the global optimal solution boundary as early as possible by optimizing the search order, thereby synergistically combining bound constraints, feasibility pruning, and optimality pruning to achieve multi-dimensional dynamic pruning of the state space tree.
NOIP Practical Pitfall Guide
1. Search Order
In the absence of dependencies, decisions with stronger "constraint capabilities" (such as larger size or resource consumption) should be prioritized for enumeration.
The key to adjusting the search order from large to small is twofold: on one hand, large-scale decisions can rapidly consume resources, causing the search space of subsequent subtrees to collapse dramatically; on the other hand, the algorithm can quickly reach leaf nodes early in the process, obtaining a high-quality baseline solution, thus significantly tightening the global upper bound ans of the objective function and activating subsequent high-probability, large-scale pruning.
Conversely, if enumerating from small to large, the slow resource consumption of shallow-level decisions leads to exponential expansion of the state space at shallow levels; simultaneously, the inability to update ans promptly paralyzes the optimality pruning throughout the entire search process.
2. Integer Truncation and Correctness of Evaluation Functions
When performing mathematical scaling for optimality pruning, the integer division operation in the expression s + 2 * (N - v) / r introduces floor truncation errors. Since this arithmetic truncation effect makes the estimated lower bound of future cost conservative, it will not mistakenly prune away a potentially optimal solution; it only slightly relaxes the boundary condition, making it safe in terms of algorithmic correctness.
When performing high-power geometric volume calculations, if the data boundaries are large, intermediate variables must be explicitly cast to high-precision types such as long long. Otherwise, feasibility pruning will fail and cause TLE.
Classic Problems
1. Luogu P1120 Small Sticks
- Problem Description: Several wooden sticks of equal length are randomly cut into $N$ small sticks of length no greater than 50. Given the lengths of the cut sticks, find the minimum possible length of the original sticks.
- Problem Essence: Equal-sum subset partition problem of a multiset.
- Pruning Strategies:
- Bound Pruning: The physical boundary of the original length $L$ must satisfy $L \in [\max(stick), \sum stick]$. Also, since all original sticks are of equal length, when enumerating $L$, it must satisfy $\sum stick \pmod L == 0$.
- Optimize Search Order: Sort the small stick lengths in descending order, prioritizing longer sticks first. Longer sticks have "stronger physical constraints and less combinatorial flexibility." Placing longer sticks first causes the search space of subsequent subtrees to collapse dramatically; it also avoids the formation of lush, inefficient shallow-level branches from many short sticks.
- Feasibility Pruning: Redundant value elimination. If the current
stick[i]fails, then all subsequent sticks of the same length will necessarily fail, so directly skip them using awhileloop.
/**
* @param res_sticks Number of original sticks left to assemble
* @param cur_len Accumulated length of the current original stick being assembled
* @param last_idx Starting index for enumeration in the current level (avoid repeated combinations)
*/
bool dfs(int res_sticks, int cur_len, int last_idx) {
if (res_sticks == 0) return true;
// Current original stick is perfectly assembled, start assembling a new one
if (cur_len == target_len) {
return dfs(res_sticks - 1, 0, 0);
}
for (int i = last_idx; i < n; ++i) {
if (visited[i] || cur_len + stick[i] > target_len) continue;
visited[i] = true;
if (dfs(res_sticks, cur_len + stick[i], i + 1)) return true;
visited[i] = false;
// 1. First stick and last stick failure pruning
if (cur_len == 0 || cur_len + stick[i] == target_len) return false;
// 2. Redundant value pruning (skip subsequent sticks of the same length)
while (i + 1 < n && stick[i] == stick[i + 1]) {
i++;
}
}
return false;
}
if (cur_len == 0 || cur_len + stick[i] == target_len) return false;
First stick failure means: "Under the most lenient conditions (a completely new empty stick), any combination involving this first stick cannot complete the assembly of all remaining sticks." Last stick failure means: "After this stick perfectly fills the current $L$, the assembly of subsequent sticks still experiences global failure."
2. Luogu P1434 Skiing
- Problem Description: Given a 2D grid where each cell has a height. You can only slide from higher to lower in four directions. Find the length of the longest sliding path.
- Problem Essence: Longest path search on a DAG (Directed Acyclic Graph).
- Core Pruning Strategy:
Memoization Pruning: A type of state deduplication pruning. Define
f[x][y]as the longest path starting from(x, y). Iff[x][y]has already been computed, directly return its value, pruning the exponential search tree down to $O(N \times M)$ linear graph traversal.
int dfs(int x, int y) {
if (f[x][y] > 0) return f[x][y]; // Memoization pruning, directly return known state
int max_len = 1;
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i], ny = y + dy[i];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && grid[nx][ny] < grid[x][y]) {
max_len = std::max(max_len, dfs(nx, ny) + 1);
}
}
return f[x][y] = max_len; // Record the optimal solution for the current state
}