NeFut Logo NeFut
EN 管理员登录

动态规划中的顺推与逆推机制详解

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

核心逻辑与数学原理

在确定了状态空间与 DAG 拓扑序后,实现状态转移有两种底层截然相反的动态规划驱动机制:顺推(人人为我,Pull 型)逆推(我为人人,Push 型)。两者在数学本质上是对 DAG 边集的不同遍历视角。

  1. 顺推(人人为我 / Pull)

    • 核心数学形式:$f[i] = \text{opt}_{j \in prev(i)} \{ f[j] + w(j, i) \}$
    • 逻辑视角:当前状态 $i$ 作为接受者。为了计算 $f[i]$,主动检索并拉取(Pull)所有能够到达 $i$ 的前驱状态 $j$ 的已知信息。
    • 适用场景:前驱状态的边界极其明确、规则固定的问题。
  2. 逆推(我为人人 / Push)

    • 核心数学形式:对于当前已固化的状态 $j$,更新它所有能到达的后继状态 $i$:$f[i] = \text{opt}(f[i], f[j] + w(j, i))$
    • 逻辑视角:当前状态 $j$ 作为贡献者。当 $f[j]$ 的值确定后,主动将其价值推送(Push)并贡献给所有以 $j$ 为起点的后继状态 $i$。
    • 适用场景:当前状态所能带来的“后续影响”很容易枚举,但若反过来找“谁能转移到当前状态”却极其困难或复杂的场景(如刷表法、稀疏转移、博弈论状态变换)。

状态设计与算法推导

以经典的“数字三角形 / 变长跳跃路径最大收益”模型来拆解两种推导逻辑。假设在序列中,从位置 $i$ 可以跳跃到 $i+1$ 或 $i+2$,代价为 $w(i, \text{target})$。

1. 顺推(人人为我)设计

$$f[i] = \max(f[i-1] + w(i-1, i), f[i-2] + w(i-2, i))$$

2. 逆推(我为人人)设计

$$f[i+1] = \max(f[i+1], f[i] + w(i, i+1))$$

$$f[i+2] = \max(f[i+2], f[i] + w(i, i+2))$$


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

以下源码在同一拓扑图(一维多步非固定跳跃模型)下,分别给出“顺推”与“逆推”的完整实现,对比其底层逻辑。

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

using namespace std;

const int MAXN = 10005;
const int INF = 0x3f3f3f3f;

int w[MAXN]; // 节点自身的价值
int f_pull[MAXN]; // 顺推数组
int f_push[MAXN]; // 逆推数组

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

    int n;
    if (!(cin >> n)) return 0;
    for (int i = 1; i <= n; ++i) {
        cin >> w[i];
    }

    // ==================== 1. 顺推写法 (人人为我) ====================
    // 必须严密控制前驱合法性
    f_pull[1] = w[1];
    f_pull[2] = w[1] + w[2]; 
    for (int i = 3; i <= n; ++i) {
        // 主动拉取前驱 i-1 和 i-2 的状态信息
        f_pull[i] = max(f_pull[i - 1], f_pull[i - 2]) + w[i];
    }

    // ==================== 2. 逆推写法 (我为人人 / 刷表法) ====================
    // 必须将未达状态初始化为极值,防止从不合法状态向后贡献
    for (int i = 1; i <= n; ++i) f_push[i] = -INF;

f_push[1] = w[1]; // 唯一合法的起点
    for (int i = 1; i < n; ++i) {
        if (f_push[i] == -INF) continue; // 致命踩坑点:跳过不可达的虚无状态

        // 用当前确定的状态,主动去刷新后继状态的值
        if (i + 1 <= n) f_push[i + 1] = max(f_push[i + 1], f_push[i] + w[i + 1]);
        if (i + 2 <= n) f_push[i + 2] = max(f_push[i + 2], f_push[i] + w[i + 2]);
    }

    cout << f_pull[n] << " " << f_push[n] << "\n";
    return 0;
}

NOIP 实战避坑指南

1. 逆推(我为人人)未剔除“不合法/不可达”前驱状态

2. 顺推(人人为我)边界状态越界强行读取


经典 NOIP/洛谷 真题

1. 洛谷 P1004 [NOIP2000 提高组] 方格取数

2. 洛谷 P1063 [NOIP2006 提高组] 能量项链

原文链接: local://13.4

[h] 返回首页