路径按列推进、每列只取入口行到出口行的连续段:按列 DP,用前缀 max 与后缀 max 把列内转移压到 O(n),总复杂度 O(nm)。
OJ: roj
题目 ID: 3667
难度:普及+/提高-
标签:动态规划线性DP网格python
创建: 2026-10-02 14:40
更新: 2026-10-10 11:30
形式化题目
给定
样例 1 中一条和为 9 的最优路径如下,加粗为路径经过的格子:
| 行\列 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 1 | 1 | 3 | 2 | |
| 2 | 2 | -1 | 4 | -1 |
| 3 | -1 |
路径为
正解
思路
关键观察:路径可以按列拆成连续段。
- 向右是唯一改变列号的走法,且只能让列号
、不能减小。从第 1 列到第 列恰好需要 次向右,因此列号序列为 逐列推进:每一列恰好进入一次、离开一次,不存在跳列或回访(回访需要向左)。 - 进入一列后、离开它之前只能在列内竖直移动;不重访要求列内不能折返(折返必重复经过格子),所以列内访问的格子必然是入口行到出口行之间的一段连续区间,且走法唯一——在列内"怎么走"没有自由度,只有"从哪行进、从哪行出"两个参数。
- 第
列的入口行等于第 列的出口行(列间只能同行向右移动),第 1 列从第 1 行进入,第 列的出口行必须是第 行。
于是整条路径与"每列的出口行序列"
朴素做法。 直接枚举出口行序列是
段和
优化:把两层枚举拆成两个单调扫描。 瓶颈在于每个
(入口在上方,向下走到 ):
大括号与
(入口在下方,向上走到 ):
同理自下而上维护后缀最大值。
两半恰好覆盖全部入口行(
样例 1 逐列的 DP 值如下,行为出口行(1…3)、列为"离开第
| 离开列 |
行 1 | 行 2 | 行 3 |
|---|---|---|---|
| 1 | 1 | 3 | 1 |
| 2 | 1 | 2 | 4 |
| 3 | 9 | 8 | 5 |
| 4 | 11 | 10 | 9(答案) |
第 1 行是第 1 列从第 1 行走到第
初值也要小心:权值可以为负,DP 里没有"空段"可选,每个状态都必须真实走满入口到出口的段,因此初始值就是第 1 列的真实段和(可以是负数),扫描用的哨兵只取一个远小于
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NEG=-(ll)1000000000000000000LL;
vector<ll> advance_col(vector<ll>&dp, vector<ll>&pre){int n=dp.size(); vector<ll> lo(n),hi(n),res(n); ll best=NEG; for(int r=0;r<n;r++){best=max(best,dp[r]-pre[r]); lo[r]=best;} best=NEG; for(int r=n-1;r>=0;r--){best=max(best,dp[r]+pre[r+1]); hi[r]=best;} for(int c=0;c<n;c++) res[c]=max(pre[c+1]+lo[c], hi[c]-pre[c]); return res;}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int n,m; if(!(cin>>n>>m)) return 0; vector<vector<ll> > pre(m, vector<ll>(n+1)); for(int i=0;i<n;i++) for(int j=0;j<m;j++){ll v;cin>>v; pre[j][i+1]=pre[j][i]+v;} vector<ll> dp(n); for(int i=0;i<n;i++) dp[i]=pre[0][i+1]; for(int j=1;j<m;j++) dp=advance_col(dp,pre[j]); cout<<dp[n-1]<<'\n'; return 0;}#!/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-10-02 14:42
# update_at: 2026-10-02 14:42
import sys
NEG = -10**18 # max 扫描的初值:任何真实路径和都远大于它
def advance(dp: list[int], pre: list[int]) -> list[int]:
"""按列转移一列:由上一列每行的最优值 dp 推出本列每行的最优值。
pre 是本列前缀和,pre[i] = 前 i 行之和(pre[0] = 0)。从进入行 r 走到离开行 c
收集的格子是本列 [min(r,c), max(r,c)] 这一段,按 r <= c 与 r >= c 拆成两半,
各用一次单调扫描取 max,不必两两枚举 (r, c)。
"""
n = len(dp)
lo: list[int] = [] # lo[c] = max_{r<=c} (dp[r] - pre[r]):进来的行在上方
best = NEG
for r, value in enumerate(dp):
best = max(best, value - pre[r])
lo.append(best)
hi: list[int] = [] # hi[c] = max_{r>=c} (dp[r] + pre[r+1]):进来的行在下方
best = NEG
for r in range(n - 1, -1, -1):
best = max(best, dp[r] + pre[r + 1])
hi.append(best)
hi.reverse() # 倒着扫完再翻转,让下标对齐离开行 c
return [max(pre[c + 1] + lo[c], hi[c] - pre[c]) for c in range(n)]
def solve() -> None:
data = sys.stdin.buffer
n, m = map(int, data.readline().split())
# 按行读入、按列存前缀和:pre[j][i] = 第 j 列前 i 行之和
pre = [[0] * (n + 1) for _ in range(m)]
for i in range(n):
row = list(map(int, data.readline().split()))
for j, value in enumerate(row):
pre[j][i + 1] = pre[j][i] + value
dp = pre[0][1:] # 第 1 列只能从第 1 行进入,走到第 r 行时已收集前 r 行
for j in range(1, m):
dp = advance(dp, pre[j])
print(dp[-1]) # 终点固定在第 n 行、第 m 列
if __name__ == "__main__":
solve()advance() 即上面两个扫描:lo/hi 分别是自上而下、自下而上维护的运行最大值(hi 扫完 reverse() 以对齐出口行下标),最后一行推导式对每个出口行取两半较大者;solve() 只做读入(按行读、顺手累加列前缀和)、初始化与逐列调用。唯一要注意下标约定:正文公式按 1 基行号书写,代码是 0 基下标,公式里的 pre[r]、pre[c+1],含义完全一致。
复杂度
- 时间:读入与前缀和
,逐列转移 次、每次 ,合计 ,与正解同阶。 - 空间:列前缀和表
,DP 与扫描数组 。 时实测 0.30s、峰值 53.9MB;答案量级可达 ,Python int无溢出问题(换 C++ 需long long)。
总结
- 单向(上/下/右)移动让列号单调推进,“不重访"让每列内的走法坍缩成一段连续区间——两个约束合起来,把看似指数的路径搜索压成了按列的一维 DP,状态就是"从哪行离开当前列”。
- 转移形如
且段和可按 与 的大小拆分时,把与 无关的项提出后用前缀 max / 后缀 max 各扫一遍,即可把 的列内转移降到 ;这是处理"两层枚举但被加项可分离"的通用套路。 - 易错点:终点必须取最后一列第
行的状态(中间行可能有更大的假最优);权值有负,初值与哨兵不能随手填 0。

