NeFut Logo NeFut
EN 管理员登录

深入理解记忆化搜索与拓扑排序的完美结合

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

核心逻辑与数学原理

对于许多复杂或隐蔽的状态空间,状态之间的依赖关系并不像线性循环那样直观。如果强行编写嵌套循环,极易因难以确定计算顺序而导致使用未固化的状态。

记忆化搜索(Memoization)与拓扑排序(Topological Sort)在哲学上是完全等价的:

  1. 拓扑序的本质:若状态 $A$ 的计算依赖于状态 $B$,则在状态图(DAG)中必有一条有向边 $B \to A$。拓扑排序的目的是建立一个全序序列,使得所有边的起点都排在终点之前。
  2. 递归形式的拓扑排序(DFS):记忆化搜索通过动态规划的逆向视角(从目标状态出发,递归询问前驱状态),利用深度优先搜索(DFS)的系统栈隐式完成状态图的拓扑排序。当一个状态的所有后继/前驱状态都访问完毕、即将弹栈时,该状态的全局最优解就已经固化。
  3. 消除冗余的核心算子:记忆化搜索通过引入一个与状态空间完全同构的记忆化数组 f(通常初始化为未访问标记,如 -1)。在递归分支中,一旦发现 f[state] 已经被计算过,则立即 $O(1)$ 返回。这在几何上相当于直接剪掉了 DAG 中所有重复访问的子图,将搜索的指数级复杂度硬性拽回图的阶数与边数之和:$O(|V| + |E|)$。

状态设计与算法推导

以经典图论模型“滑雪”(DAG 最长路)为例,推导记忆化搜索的执行过程:

1. 状态设计

设状态 $f[i][j]$ 表示从网格坐标 $(i, j)$ 出发所能滑行的最大区域长度(即以该格点为起点的最长路径边数 $+ 1$)。

2. 转移方程与递归形式

由于只能向地势低的方向滑行,定义 $(nx, ny)$ 为 $(i, j)$ 的四周邻居且满足 $h[nx][ny] < h[i][j]$。转移方程表现为典型的后继最大值松弛:

$$f[i][j] = 1 + \max_{(nx, ny) \in \text{valid\_neighbors}(i, j)} \{ f[nx][ny] \}$$

若四周没有比当前点更低的地势,则该格点为 DAG 的入度为 0 的源点(逆向看作终点), $f[i][j] = 1$。

3. 记忆化逻辑

定义搜索函数 int dfs(int x, int y)


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

以下为基于记忆化搜索的区域最长下降路径实现,严格通过 Linux g++ -O2 编译规范。

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int MAXN = 105;
int h[MAXN][MAXN];
int f[MAXN][MAXN]; // 记忆化数组,0 表示未访问过
int n, m;

// 向量方向数组
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};

int dfs(int x, int y) {
    // 致命踩坑点:若已经计算过,必须直接返回,否则记忆化失效退化为纯暴力搜索
    if (f[x][y] != 0) {
        return f[x][y];
    }

    f[x][y] = 1; // 初始化:自身至少算一个长度

    for (int i = 0; i < 4; ++i) {
        int nx = x + dx[i];
        int ny = y + dy[i];

        // 边界检查与严格下滑条件
        if (nx >= 1 && nx <= n && ny >= 1 && ny <= m) {
            if (h[nx][ny] < h[x][y]) {
                f[x][y] = max(f[x][y], dfs(nx, ny) + 1); // 隐式拓扑序转移
            }
        }
    }

    return f[x][y]; // 此时该节点在 DAG 中的最长路已完全固化
}

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

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

    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            cin >> h[i][j];
            f[i][j] = 0; // 0 初始化表示所有状态未求值
        }
    }

    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            ans = max(ans, dfs(i, j)); // 多源 DFS 寻找全局最长路
        }
    }

    cout << ans << "\n";

    return 0;
}

NOIP 实战避坑指南

1. 记忆化数组未赋予正确的“未访问”标记值

2. 状态转移图中存在隐蔽环路导致系统栈溢出


经典 NOIP/洛谷 真题

1. 洛谷 P1434 [SHOI2002] 滑雪

2. 洛谷 P1464 Function

原文链接: local://13.3

[h] 返回首页