题解:洛谷 P3842 线段

【题目来源】

洛谷:P3842 [TJOI2007] 线段

【题目描述】

在一个 \(n \times n\) 的平面上,在每一行中有一条线段,第 \(i\) 行的线段的左端点是 \((i, L_{i})\),右端点是 \((i, R_{i})\)

你从 \((1,1)\) 点出发,要求沿途走过所有的线段,最终到达 \((n,n)\) 点,且所走的路程长度要尽量短。

更具体一些说,你在任何时候只能选择向下走一步(行数增加 \(1\))、向左走一步(列数减少 \(1\))或是向右走一步(列数增加 \(1\))。当然,由于你不能向上行走,因此在从任何一行向下走到另一行的时候,你必须保证已经走完本行的那条线段。

【输入】

第一行有一个整数 \(n\)

以下 \(n\) 行,在第 \(i\) 行(总第 \((i+1)\) 行)的两个整数表示 \(L_i\)\(R_i\)

【输出】

仅包含一个整数,你选择的最短路程的长度。

【输入样例】

6
2 6
3 4
1 3
1 2
3 6
4 5

【输出样例】

24

【核心思想】

  1. 问题分析:给定 \(n \times n\) 平面上每行一条线段 \([L_i, R_i]\),从 \((1,1)\) 出发,只能向下、向左、向右走,要求完整走过每行线段后到达 \((n,n)\),求最短路程。这是一个线性 DP 问题,关键在于状态设计记录每行结束位置(左端点或右端点),利用线段必须完整走过的约束进行递推。

  2. 算法选择

    • 线性 DP(一维扩展):设 \(f[i][0/1]\) 表示处理完前 \(i\) 行线段,停在第 \(i\) 行左端点/右端点的最短距离
    • 虚拟终点行:增设第 \(n+1\) 行,左右端点均为 \(n\),统一处理到达 \((n,n)\) 的终点约束
  3. 关键步骤

    • 初始化:读取 \(n\) 和每行线段 \([L_i, R_i]\),设 \(L_0 = R_0 = 1\)(起点),\(L_{n+1} = R_{n+1} = n\)(虚拟终点)
    • DP 初始化\(f[0][0] = 0\),其余为 \(\infty\)
    • 状态转移(遍历 \(i\)\(1\)\(n+1\)):
      • 停在左端点 \(f[i][0]\):必须从第 \(i\) 行右端点走过来(完整走线段),从上一行左端点或右端点转移
        • \(f[i][0] = \min(f[i-1][0] + F(L_{i-1}, R_i, L_i), f[i-1][1] + F(R_{i-1}, R_i, L_i))\)
      • 停在右端点 \(f[i][1]\):必须从第 \(i\) 行左端点走过来
        • \(f[i][1] = \min(f[i-1][0] + F(L_{i-1}, L_i, R_i), f[i-1][1] + F(R_{i-1}, L_i, R_i))\)
      • 加上向下一步\(f[i][0]++, f[i][1]++\)
    • 输出答案\(f[n+1][1] - 2\)(减去虚拟终点行多算的向下步数和终点处理)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),单层循环,每次 \(O(1)\) 转移
    • 空间复杂度:\(O(n)\)\(f\) 数组和线段端点数组
  5. 线性 DP 的核心思想

    • 状态压缩:每行线段只需记录停在左端点还是右端点,因为完整走过线段后必然停在某一端点
    • 曼哈顿距离累加:辅助函数 \(F(a, b, c) = |a-b| + |b-c|\) 计算从上一行结束位置走到当前行线段一端,再走到另一端的距离
    • 完整走线段的约束:必须从一个端点进入,走到另一端点,因此状态转移总是"从上一行某端点 \(\to\) 当前行线段另一端点 \(\to\) 当前行目标端点"
    • 虚拟终点统一处理:增设虚拟行避免终点特判,最后减去多算的步数
    • 适用于有路径约束、必须完整覆盖区间、逐行递推的最优化问题

【算法标签】

普及 #线性DP-一维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 20005;    // 定义数组最大容量为20005
int n;                  // n为平面的行数(也是列数)
int l[N], r[N];         // l[i]和r[i]分别表示第i行线段的左端点和右端点
int f[N][2];            // f[i][0]表示走到第i行并停在线段左端点l[i]的最短距离;
                        // f[i][1]表示走到第i行并停在线段右端点r[i]的最短距离

// 辅助函数:计算从a走到b再走到c的总曼哈顿距离
int F(int a, int b, int c)
{
    return abs(a - b) + abs(b - c);  // |a-b| + |b-c|
}

int main()
{
    ios::sync_with_stdio(false);     // 关闭cin与stdio的同步,加速输入
    cin.tie(nullptr);                // 解除cin与cout的绑定,进一步加速

    cin >> n;                        // 读入行数n
    for (int i = 1; i <= n; i++)     // 读入每行线段的左右端点
        cin >> l[i] >> r[i];

    // 初始化起点:第0行在位置1(即从(1,1)出发)
    l[0] = r[0] = 1;
    // 虚拟第n+1行在位置n,用于统一处理终点(n,n)
    l[n + 1] = r[n + 1] = n;

    memset(f, 0x3f, sizeof(f));      // 将f数组初始化为极大值(0x3f3f3f3f)
    f[0][0] = 0;                     // 第0行停在位置1,初始距离为0

    // 动态规划:逐行处理,从第1行到第n+1行(n+1为虚拟终点行)
    for (int i = 1; i <= n + 1; i++)
    {
        // 状态转移:走到第i行并停在线段左端点l[i]
        // 必须从第i行的右端点r[i]走过来(因为要完整走过第i行的线段)
        // 上一行停在左端点,走到当前行右端点,再走到左端点
        f[i][0] = min(f[i][0], f[i - 1][0] + F(l[i - 1], r[i], l[i]));
        // 上一行停在右端点,走到当前行右端点,再走到左端点
        f[i][0] = min(f[i][0], f[i - 1][1] + F(r[i - 1], r[i], l[i]));

        // 状态转移:走到第i行并停在线段右端点r[i]
        // 必须从第i行的左端点l[i]走过来
        // 上一行停在左端点,走到当前行左端点,再走到右端点
        f[i][1] = min(f[i][1], f[i - 1][0] + F(l[i - 1], l[i], r[i]));
        // 上一行停在右端点,走到当前行左端点,再走到右端点
        f[i][1] = min(f[i][1], f[i - 1][1] + F(r[i - 1], l[i], r[i]));

        // 加上从第i-1行向下走到第i行的1步
        f[i][0]++;
        f[i][1]++;
    }

    // f[n+1][1]包含了从第n行向下走到虚拟第n+1行的1步
    // 实际只需要走到(n,n),不需要走到(n+1,n),需要减去多算的2步
    // (向下走到n+1行的1步 + 最后停在右端点的额外处理)
    cout << f[n + 1][1] - 2 << endl;

    return 0;
}

【运行结果】

6
2 6
3 4
1 3
1 2
3 6
4 5
24
posted @ 2026-08-11 11:27  团爸讲算法  阅读(10)  评论(0)    收藏  举报