[NOIP2008-提高] 传纸条

GitHub跳转原题关系图返回列表

按行号和 x+y 把两条单调路径同步分层,用行号对 (x1,x2) 做二维 DP,重合格只计一次好感度,O((m+n)·m²) 求最大好感度和。

OJ: roj

题目 ID: 3564

难度:普及+/提高-

标签:动态规划

创建: 2026-10-02 08:00

更新: 2026-10-10 11:30

形式化题目

给定 m×nm \times n 网格,格子 (i,j)(i,j) 有权值 ai,j∈[0,100]a_{i,j} \in [0,100](两端点 (1,1)(1,1)、(m,n)(m,n) 的权值为 00)。求两条都从 (1,1)(1,1) 走到 (m,n)(m,n)、每步只能向下或向右的路径,使两条路径覆盖格子的并集权值和最大:同一个同学只会帮忙一次,两路重合的格子只计一次权值。

回信方向(向上/向左)整体反转后与去程完全同型、覆盖格子不变,所以只需求上述两条单调路径。1⩽m,n⩽501 \leqslant m,n \leqslant 50。

样例输入 3 3 / 0 3 9 / 2 8 5 / 5 7 0,输出 34。下表展示样例的一组最优方案,路径 A 与路径 B 覆盖的格子及各自权值如下:

样例最优路径 列 1 列 2 列 3
行 1 0(两路起点) 3(A) 9(A)
行 2 2(B) 8(B) 5(A)
行 3 5(未使用) 7(B) 0(两路终点)

路径 A 为「右→右→下→下」,权值 3+9+5=173+9+5=17;路径 B 为「下→右→下→右」,权值 2+8+7=172+8+7=17。两路除端点外不重合,总和 17+17=3417+17=34,恰好用满除 (3,1)(3,1) 外的全部格子。读者可先自行验证:任何想把 (3,1)=5(3,1)=5 也纳入的走法,都会迫使某条路让出更大的权值。

正解

思路

朴素做法:分别枚举两条路径再求并集和。路径条数为 (m+n−2m−1)\binom{m+n-2}{m-1},m=n=50m=n=50 时已是 101410^{14} 量级,不可行。

瓶颈与观察:整条路径的差别只通过「当前走到哪」影响后续,所以不该枚举整条路径,而应记录位置。进一步注意到:每走一步,格子的行号与列号之和 k=x+yk=x+y 恰好加 11。让两条路径同步推进,任一时刻两路的 kk 必然相同——第 kk 层的格子形如 (x, k−x)(x,\,k-x),列号由行号唯一决定,于是「两条路径」被压缩成一层内的一对行号 (x1,x2)(x_1,x_2)。不同路径对只要落在同一行号对上,后续选择完全相同,把最优值留下来即可消除重复枚举,这就是 DP。

状态:fk(x1,x2)f_k(x_1,x_2) 表示两路分别走到 (x1,k−x1)(x_1,k-x_1) 与 (x2,k−x2)(x_2,k-x_2) 时,已覆盖格子并集的最大权值和。

转移:(x,k−x)(x,k-x) 只能由上一层的 (x−1,k−x)(x-1,k-x) 向下走到达,或由 (x,k−x−1)(x,k-x-1) 向右走到达,即上一层行号 x′∈{x−1, x}x' \in \{x-1,\,x\},两路共 4 种前驱组合:

fk(x1,x2)=max⁡p1∈{x1−1, x1},  p2∈{x2−1, x2}fk−1(p1,p2)  +  ax1, k−x1  +  [x1≠x2]⋅ax2, k−x2f_k(x_1,x_2)=\max_{p_1 \in \{x_1-1,\,x_1\},\; p_2 \in \{x_2-1,\,x_2\}} f_{k-1}(p_1,p_2)\;+\;a_{x_1,\,k-x_1}\;+\;\big[x_1 \ne x_2\big]\cdot a_{x_2,\,k-x_2}

同层若 x1=x2x_1=x_2 则列号也相同,两路落在同一格——只加一次权值,正对应「每人只帮一次」。x−1=0x-1=0 是越界前驱,该格不可达,取 max⁡\max 时自然淘汰。

初始化与答案:k=2k=2 时两路都在 (1,1)(1,1),f2(1,1)=a1,1=0f_2(1,1)=a_{1,1}=0;逐层推进到 k=m+nk=m+n,两路行号都为 mm(列号 nn),答案即 fm+n(m,m)f_{m+n}(m,m)。

样例的分层过程:下面两张表是样例的 f3f_3、f4f_4,行是 x1x_1、列是 x2x_2,表头括号内为该行号在本层对应的列号 y=k−xy=k-x:

f3f_3(x=1→y=2, x=2→y=1x{=}1{\to}y{=}2,\ x{=}2{\to}y{=}1) x2=1x_2=1 x2=2x_2=2
x1=1x_1=1 3 5
x1=2x_1=2 5 2
f4f_4(x=1→y=3, x=2→y=2, x=3→y=1x{=}1{\to}y{=}3,\ x{=}2{\to}y{=}2,\ x{=}3{\to}y{=}1) x2=1x_2=1 x2=2x_2=2 x2=3x_2=3
x1=1x_1=1 12 22 19
x1=2x_1=2 22 13 18
x1=3x_1=3 19 18 7

观察一次具体转移:f4(1,2)=max⁡{f3(1,1),f3(1,2)}+a1,3+a2,2=max⁡{3,5}+9+8=22f_4(1,2)=\max\{f_3(1,1),f_3(1,2)\}+a_{1,3}+a_{2,2}=\max\{3,5\}+9+8=22,胜出前驱是 f3(1,2)f_3(1,2),即路 1 从 (1,2)(1,2) 向右走到 (1,3)(1,3)、路 2 从 (2,1)(2,1) 向右走到 (2,2)(2,2)。再看对角线:f4(2,2)=max⁡{3,5,5,2}+8=13f_4(2,2)=\max\{3,5,5,2\}+8=13,两路重合格 (2,2)(2,2) 的 88 只加了一次。层层推进后 f5(2,3)=f5(3,2)=34f_5(2,3)=f_5(3,2)=34(两路分别到 (2,3)(2,3)、(3,2)(3,2)),最终 f6(3,3)=34f_6(3,3)=34,与样例一致。

代码

按上述分层 DP 实现:advance 完成一层转移,solve 读入后逐层滚动并输出终点状态。

cpp
/**
 * 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 int NEG = -1000000000;

int m, n;
int grid[55][55];
int layer[55][55], nlayer[55][55];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> m >> n;
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            cin >> grid[i][j];

    for (int i = 0; i <= m; i++)
        for (int j = 0; j <= m; j++)
            layer[i][j] = NEG;
    layer[1][1] = grid[0][0];

    for (int k = 3; k <= m + n; k++) {
        for (int i = 0; i <= m; i++)
            for (int j = 0; j <= m; j++)
                nlayer[i][j] = NEG;
        int lo = max(1, k - n), hi = min(m, k - 1);
        for (int x1 = lo; x1 <= hi; x1++) {
            int v1 = grid[x1 - 1][k - x1 - 1];
            for (int x2 = lo; x2 <= hi; x2++) {
                int best = NEG;
                for (int p1 = x1; p1 >= x1 - 1 && p1 >= 1; p1--)
                    for (int p2 = x2; p2 >= x2 - 1 && p2 >= 1; p2--)
                        if (layer[p1][p2] > best) best = layer[p1][p2];
                if (best == NEG) continue;
                int gain = v1 + (x1 != x2 ? grid[x2 - 1][k - x2 - 1] : 0);
                nlayer[x1][x2] = best + gain;
            }
        }
        for (int i = 0; i <= m; i++)
            for (int j = 0; j <= m; j++)
                layer[i][j] = nlayer[i][j];
    }
    cout << layer[m][m] << "\n";
    return 0;
}
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-10-02 08:02
# update_at: 2026-10-02 08:02

import sys

NEG = -10**9  # 不可达状态的哨兵(好感度非负,可达值恒 >= 0)


def advance(prev: list[list[int]], grid: list[list[int]], k: int, m: int, n: int) -> list[list[int]]:
    """推进一层:求两条路的行号和都恰好为 k 时,能拿到的最大好感度和。

    行号和为 k 的格子 (x, k-x) 只能由上一层的 (x-1, k-x) 向下走到达,
    或由 (x, k-x-1) 向右走到达,所以两条路各自只需尝试两种来向,共 4 种组合。
    """
    cur = [[NEG] * (m + 1) for _ in range(m + 1)]
    lo, hi = max(1, k - n), min(m, k - 1)  # 第 k 层合法行号:列号 k-x 必须落在 1..n
    for x1 in range(lo, hi + 1):
        v1 = grid[x1 - 1][k - x1 - 1]  # 路 1 所在格子的好感度
        for x2 in range(lo, hi + 1):
            best = max(
                prev[p1][p2]
                for p1 in (x1, x1 - 1)  # 来向:上一层同行(向右走来)/ 上一层上一行(向下走来)
                for p2 in (x2, x2 - 1)
            )
            if best == NEG:  # 两条路都到不了的格子对,整格跳过
                continue
            # x1 == x2 时两路重合格子(同层列号也相同),每人只帮一次,好感度只加一次
            cur[x1][x2] = best + v1 + (grid[x2 - 1][k - x2 - 1] if x1 != x2 else 0)
    return cur


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    m, n = next(data), next(data)
    grid = [[next(data) for _ in range(n)] for _ in range(m)]

    # 按行号和 k 分层的二维 DP:f[x1][x2] 表示两条路分别走到 (x1, k-x1)、(x2, k-x2)
    # 时的最大好感度和。起点 k=2:两路都在 (1,1),端点好感度为 0。
    layer = [[NEG] * (m + 1) for _ in range(m + 1)]
    layer[1][1] = grid[0][0]
    for k in range(3, m + n + 1):
        layer = advance(layer, grid, k, m, n)
    print(layer[m][m])


if __name__ == "__main__":
    solve()

复杂度

  • 时间:O((m+n)⋅m2)O((m+n)\cdot m^2)——共 O(m+n)O(m+n) 层,每层 O(m2)O(m^2) 个行号对、每对 4 个前驱;m=n=50m=n=50 时约 10610^6 次基本操作。
  • 空间:O(m2)O(m^2)——只滚动相邻两层两张表。

该复杂度与正解(C++ 同款分层 DP)同阶;本地跑 10 组真实数据单例均不足 0.05 s、峰值内存约 15 MB,在 1000 ms / 32 MB 限制内。

总结

  • 回信路径反转后,问题化为两条 (1,1)→(m,n)(1,1) \to (m,n) 单调路径的并集权值最大化,重合格只计一次。
  • 单调性给出天然分层 k=x+yk=x+y:同步推进后状态只需一对行号 (x1,x2)(x_1,x_2),转移只看上一层 4 个前驱,x1=x2x_1=x_2 时少加一项即完成重合处理。
  • 复杂度 O((m+n)⋅m2)O((m+n)\cdot m^2) 时间、O(m2)O(m^2) 空间,对 m,n⩽50m,n \leqslant 50 绰绰有余。
  • 这类「多条路径同步分层」的技巧可推广到费用流、双人取数等「多主体在网格/序列上同步推进」的问题。