两条 (1,1)→(n,n) 的单调路径按步数同步推进,压成反对角线上的三维 DP f[i][j],重合格用 i≠j 去重。
OJ: roj
题目 ID: 1277
难度:普及+/提高-
标签:动态规划多线程DP网格DPpython
创建: 2026-09-30 03:03
更新: 2026-10-07 11:01
形式化题目
给定
格子里的数只能被取走一次:某个格子被两条路先后经过时,只有第一次经过算数
(等价于"取走后变为
其中
本题数据规模很小(
样例(67。下面用一个小例子说明"重合格只算一次"这个题眼。
样例讲解:取
| 列 1 | 列 2 | 列 3 | |
|---|---|---|---|
| 行 1 | 10 | 0 | 5 |
| 行 2 | 0 | 6 | 4 |
| 行 3 | 8 | 2 | 0 |
两条最优路径分别是
: ,取到 ; : ,取到 。
两条路只在起点
正解
思路
朴素做法是枚举所有路径对:
从
它失败的根源是把两条路径当成两个互相独立的对象。于是状态里要同时记录
“第一条走到哪、第二条走到哪”,也就是
关键观察:从
所以可以让两条路齐步走:走完
转移时,一条路到达
最后一项的指示函数
初始状态
下表是上面
| 位置(终点 1 / 终点 2) | 收益 | |
|---|---|---|
| 1, 1 | 15 | |
| 1, 2 | 21 | |
| 1, 3 | 23 | |
| 2, 3 | 24 | |
| 3, 3 | 18 |
例如
继续推到
t=1 state(1,1) cells (1,1) / (1,1) f=10 两条路都在起点,10 只算一次
t=2 state(1,2) cells (1,2) / (2,1) f=10 两条路在起点之后分道
t=3 state(2,3) cells (2,2) / (3,1) f=24 取到 6 与 8,两格不同,各自计入
t=4 state(2,3) cells (2,3) / (3,2) f=30 取到 4 与 2,两路分开到最远
t=5 state(3,3) cells (3,3) / (3,3) f=30 同时抵达 B,格子无值状态里的 (2,3) 在
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 03:03
# update_at: 2026-09-30 03:06
import sys
def read_grid(n: int, data: list[int]) -> list[list[int]]:
"""把三元组列表铺成 (n+2)×(n+2) 的网格,下标从 1 开始,界外补 0。
以 `0 0 0` 结束;收尾行可能只给出一两个 0,用 zip 按位置切三元组即可自动丢弃余数。
"""
grid = [[0] * (n + 2) for _ in range(n + 2)]
triples = zip(*[iter(data)] * 3) # 每 3 个相邻元素一组,余数自动丢弃
for row, col, value in triples:
if not (row or col or value): # 0 0 0 是结束标记
break
grid[row][col] = value # 重复给出的格子以后出现的数为准
return grid
def best_two_paths(grid: list[list[int]], n: int) -> int:
"""两条 (1,1)→(n,n) 只向右/向下的路径能取得的最大数字和(同格只算一次)。
两条路都走 s-1 步时,各自终点必在第 s 条反对角线 i+j=s 上,所以按步数同步推进:
f[i][j] = 两条路分别停在 (i, s-i) 与 (j, s-j) 时的最大收益,s 为当前反对角线。
这样状态从 O(n^4) 降到每层 O(n^2)、总和 O(n^3),且天然保证两条路同步。
"""
neg = -1 # 不可达状态用负数标记:本题所有数值非负,"有限"与"不可达"因此可区分
f = [[neg] * (n + 1) for _ in range(n + 1)]
f[1][1] = grid[1][1] # 两条路都从 A 出发,起点数字只计一次
for s in range(3, 2 * n + 1): # 反对角线 i+j=2 上两条路都在 (1,1),不必转移
nf = [[neg] * (n + 1) for _ in range(n + 1)]
rows = range(max(1, s - n), min(n, s - 1) + 1) # 该反对角线上合法的行号
for i, j in ((i, j) for i in rows for j in rows):
# 两条路各自"上一步"只能从左边或上边来,共 4 种组合
best = max(f[i - 1][j - 1], f[i - 1][j], f[i][j - 1], f[i][j])
if best >= 0:
gain = grid[i][s - i] + (0 if i == j else grid[j][s - j]) # 同格只取一次
nf[i][j] = max(nf[i][j], best + gain)
f = nf
return f[n][n]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
print(best_two_paths(read_grid(n, list(data)), n))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-04 23:43
* update_at: 2026-10-04 23:43
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 15;
int n;
int a[MAXN][MAXN]; // a[i][j] 表示格子 (i, j) 上的数字(未填的格子为 0)
int f[MAXN][MAXN]; // f[i][j]:两条路各走 s-1 步、终点行号为 i 和 j 时的最大收益(s 为当前反对角线)
int nf[MAXN][MAXN]; // nf 滚动保存下一层(反对角线 s+1)的 DP 表
// 读入网格:每行三个整数 行 列 值,"0 0 0" 结束。
void read_input() {
cin >> n;
while (true) {
int r, c, v;
cin >> r >> c >> v;
if (r == 0 && c == 0 && v == 0) break;
a[r][c] = v; // 重复给出的格子以后出现的数为准
}
}
void solve() {
// 初始:两条路都在起点 A(1,1),起点数字只计一次
// s=2 即反对角线 i+j=2,f[1][1] 表示两条路都停在 (1,1)
memset(f, -1, sizeof(f)); // -1 表示不可达状态(本题数值非负,可用 -1 区分)
f[1][1] = a[1][1];
// 两条路同步推进:走完 s-1 步时终点必在反对角线 i+j=s 上,
// 行号 i、j 定下后列号 s-i、s-j 唯一确定,状态从四维压成三维。
for (int s = 3; s <= 2 * n; s++) {
memset(nf, -1, sizeof(nf));
// 该反对角线上合法的行号范围(列号 s-i 必须落在 1..n)
int low = max(1, s - n);
int high = min(n, s - 1);
for (int i = low; i <= high; i++) {
for (int j = low; j <= high; j++) {
// 两条路各自的上一步只能从上方或左方来,共 4 种来源组合
int best = max(max(f[i - 1][j - 1], f[i - 1][j]),
max(f[i][j - 1], f[i][j]));
if (best < 0) continue; // 前一层无可达来源
// 终点 1 在 (i, s-i);终点 2 在 (j, s-j)
int gain = a[i][s - i];
if (i != j) gain += a[j][s - j]; // i==j 说明两路停在同一格,只取一次
if (best + gain > nf[i][j]) nf[i][j] = best + gain;
}
}
// 滚动:把这一层的结果作为下一层的"上一层"
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
f[i][j] = nf[i][j];
}
// 两条路同时到达 B(n,n)(反对角线 s=2n 上唯一状态)
cout << f[n][n] << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间:共
层,每层 个状态,每个状态 4 次比较的 转移,总计 。 时状态数不到 。 - 空间:滚动保存相邻两层
的 DP 表,加上网格本身,总计 。
对照:未同步的四维写法
总结
原题的状态之所以看起来必须四维,是因为"两条路"的进度被当成两个独立变量。一旦注意到
所有合法路径的步数相同、第
这类"同时进行多个同构过程、彼此有冲突(不能重复取同一格)"的题,都是同一个套路:
把多个过程按同一"时钟"同步推进,把各自的进度压进一个维度。做题时要特别留意舍去的那一项——
这里的
图示解析
这张图串起本题从题面到答案的主线:
两条路径的最大取数和
`- 朴素:枚举路径对 -> C(2N-2,N-1)^2 ~ 2.4e9,不可行
`- 瓶颈:两条路进度互相独立,状态被迫 (i,j,k,l) 四维
`- 关键观察:任意合法路径步数恒为 2N-2,第 t 步在反对角线 i+j=t+1
`- 同步推进:位置由行号 i 和步数 t 唯一确定 -> 状态 (t, i, j)
`- 转移:4 种来源取最大,增量 = a[i][t+1-i] + (i!=j) * a[j][t+1-j]
`- 去重:i==j 即同一格,只加一次(题眼)
`- 答案:f[2N-2][N][N],复杂度 O(N^3) / O(N^2)先看第一个节点:问题本身只是"取两条路",没有别的操作。第二个节点说明朴素解为什么爆炸——
不是路径多到没法枚举单条,而是路径对的组合数太大。第三个节点是本题唯一的建模决定:
把"两条路"换成"两条路齐步走",用步数这个公共时钟绑住两条路的进度。
第四个节点给出状态定义与转移,其中最难记的是
