路径型 / 递推入门
数字三角形·过河卒·方格取数
本课摘要
路径型 / 递推入门课程回答“沿序列或网格推进时,如何定义最小充分状态”。内容以数字三角形·过河卒·方格取数为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断路径型 / 递推入门的适用条件与状态边界
- 围绕“数字三角形·过河卒·方格取数”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
从「一步一步往下走」说起
先看一个具体场景——数字三角形:一座数字塔,从塔顶出发往下走,每一步只能踩到正下方或右下方那一格, 一直走到塔底。把沿途踩过的数字加起来,问:怎么走,能让这个总和最大?
第一反应也许是贪心:每一步都挑「眼前更大的那个邻居」。从顶上 往下,左边是 、右边是 ,贪心选 ; 再往下, 的两个孩子是 和 ,选 ——凑成 。这一回它恰好对了,但贪心并不可靠: 眼前小一点的邻居,底下可能接着一串大数。此刻的最优选择,要看后面还能捡到多少——这是个牵一发动全身的全局问题。
那把每条路径都枚举一遍呢?塔有 层,每步二选一,就是 条路, 时是天文数字。DP 的思路,是不去数「路」,而是给每一格算一个值:从这一格出发、走到塔底能拿到的最大总和。
这里藏着线性 DP 最朴素的一问:站在一格上,往下的「最后一步」从哪来?——只可能是它正下方或右下方的那一格接上来。 把这个「最后一步」想清楚,转移方程就浮出来了。
状态与转移:每格只回看下面两格
定状态。设 表示:从第 行第 列这一格出发、一路走到塔底,能得到的最大数字和。 这样一来,我们真正想要的答案就是塔顶那一格 。
站在 ,下一步只有两条路:走到正下方 ,或走到右下方 。 从这一格出发的最大和,就是「自己这格的数字 」加上「两个下方谁能带来更大的后续」。于是得到转移方程:
边界在最底行:站在塔底,脚下就是终点,无路可走,从这里出发的最大和就是它自己,。 有了地基,从倒数第二行开始自底向上逐行往塔顶推,最后读 即答案。
本质
这一步把「数 条路」换成了「给 个格子各算一个最优值」。能这么换,靠的是无后效性: 只关心「从这格往下」的最优, 与「怎么走到这格」毫无关系。每个子问题(一格的最优)算一次、存下来,被上方两格反复复用——这正是最优子结构 + 重叠子问题,DP 的两块基石。
跟着算一遍
用图中那座三行小塔(第 1 行 ;第 2 行 ;第 3 行 )走一遍,从最底行往上填:
看它从底往上长出来
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
换个方向:把「最优」换成「计数」
同一套「格子上的递推」,稍微换个问法就能解一类全新的题。看过河卒:一枚卒在网格上,从左上角出发,每步只能向右或向下, 要走到右下角。这回不问「最大和」,而问一共有多少条不同的路。
还是先问那句话:走到某一格 ,最后一步从哪来?只可能从上方 向下一步,或从左方 向右一步。 于是「走到 的路数」= 「走到上方的路数」+「走到左方的路数」:
边界是起点 (站着没动,也算 1 条路),第一行、第一列都只有 1 条路(只能一直往右 / 往下)。和数字三角形是同一个模具:只是把转移里的 换成了相加——求最优变成了求方案数。
过河卒还多一条硬约束:棋盘上有一匹马,马本身和它能一步跳到的 8 个点都是障碍,卒一步都不能踩。障碍怎么进方程?很简单——障碍格的方案数直接钉成 0:既然卒到不了它,它也就不会再把任何路径数往右、往下传出去。这就是「非法状态清零」,线性 DP 里最常用的一记落子。
用一个 的小网格验一下:不设障碍时,第一行、第一列全是 ,往里每格上+左累加,右下角得 (正是组合数 )。 一旦把正中间 设为障碍钉成 ,穿过中心的那些路全被掐断,右下角只剩 。一个格子清零,整张计数表随之改写。
并排看:障碍如何截断路径
道理讲完,不如亲手试。下面的网格默认 、正中 是障碍——点任意格子可设 / 撤障碍(起点「起」、终点「终」锁定不可点)。 读数条会实时告诉你:无障碍时共几条路,避开当前障碍后剩几条,被截断了多少。演示区把每格从上方 + 左方累加的过程逐格走给你看, 障碍格会标红并钉成 。试着把障碍挪到角落,或一次设两三个,看这个计数怎么随之崩塌或复原。
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
记牢:非法状态钉成「零元」
「障碍清零」不是特例,而是一条通法:在求最优的题里,非法状态钉成 (永远选不中);在求方案数的题里,钉成 (贡献 0 条路)。 把不合法的格子设成该问题的「零元」,它就会自动被排除在所有转移之外——比在每条转移里写一堆 判断干净得多。
例题
#include <algorithm>
#include <iostream>
using namespace std;
#define MX 1005
int n, a[MX][MX], f[MX][MX];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
cin >> a[i][j];
for (int i = 1; i <= n; i++) // 最底行先落地:f[n][j] = a[n][j]
f[n][i] = a[n][i];
for (int i = n - 1; i >= 1; i--) // ★自底向上,从倒数第二行往塔顶推
for (int j = 1; j <= i; j++)
f[i][j] = a[i][j] + max(f[i + 1][j], f[i + 1][j + 1]); // 正下方 / 右下方取大
cout << f[1][1] << endl; // 答案在塔顶
return 0;
}
// TAG: 线性DP 数字三角形 递推#include <iostream>
using namespace std;
#define MX 25
long long f[MX][MX]; // ★路径数会爆 int,必须 long long
bool block[MX][MX]; // 马的控制点(障碍)
int dx[9] = {0, 1, 1, 2, 2, -1, -1, -2, -2};
int dy[9] = {0, 2, -2, 1, -1, 2, -2, 1, -1};
int main()
{
int bx, by, hx, hy;
cin >> bx >> by >> hx >> hy;
bx += 1, by += 1, hx += 1, hy += 1; // 坐标从 0 起,整体平移成 1-based
for (int k = 0; k < 9; k++) // 马本身 + 8 个control点设为障碍
{
int x = hx + dx[k], y = hy + dy[k];
if (x >= 1 && y >= 1)
block[x][y] = true;
}
f[1][1] = 1; // 起点:1 条路(未走)
for (int i = 1; i <= bx; i++)
for (int j = 1; j <= by; j++)
{
if (block[i][j]) // 障碍格:卒到不了,方案数清零
{
f[i][j] = 0;
continue;
}
if (i == 1 && j == 1)
continue;
f[i][j] = f[i - 1][j] + f[i][j - 1]; // 上方来 + 左方来
}
cout << f[bx][by] << endl;
return 0;
}
// TAG: 线性DP 网格路径 计数 障碍#include <algorithm>
#include <iostream>
using namespace std;
#define MX 12
int n, a[MX][MX];
int f[MX][MX][MX][MX]; // 两条路径同时走:各自的 (x1,y1) 与 (x2,y2)
int main()
{
cin >> n;
int x, y, w;
while (cin >> x >> y >> w && (x || y || w))
a[x][y] = w;
for (int i = 1; i <= n; i++) // 两条路一起从 (1,1) 走到 (n,n)
for (int j = 1; j <= n; j++)
for (int k = 1; k <= n; k++)
for (int l = 1; l <= n; l++)
{
int best = max(max(f[i - 1][j][k - 1][l], f[i - 1][j][k][l - 1]),
max(f[i][j - 1][k - 1][l], f[i][j - 1][k][l - 1]));
f[i][j][k][l] = best + a[i][j] + a[k][l];
if (i == k && j == l) // 同一格只能被拿一次,扣掉重复
f[i][j][k][l] -= a[i][j];
}
cout << f[n][n][n][n] << endl;
return 0;
}
// TAG: 线性DP 网格路径 双线程 方格取数
