题解:洛谷 P3842 线段
【题目来源】
【题目描述】
在一个 \(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
【核心思想】
-
问题分析:给定 \(n \times n\) 平面上每行一条线段 \([L_i, R_i]\),从 \((1,1)\) 出发,只能向下、向左、向右走,要求完整走过每行线段后到达 \((n,n)\),求最短路程。这是一个线性 DP 问题,关键在于状态设计记录每行结束位置(左端点或右端点),利用线段必须完整走过的约束进行递推。
-
算法选择:
- 线性 DP(一维扩展):设 \(f[i][0/1]\) 表示处理完前 \(i\) 行线段,停在第 \(i\) 行左端点/右端点的最短距离
- 虚拟终点行:增设第 \(n+1\) 行,左右端点均为 \(n\),统一处理到达 \((n,n)\) 的终点约束
-
关键步骤:
- 初始化:读取 \(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[i][0]\):必须从第 \(i\) 行右端点走过来(完整走线段),从上一行左端点或右端点转移
- 输出答案:\(f[n+1][1] - 2\)(减去虚拟终点行多算的向下步数和终点处理)
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),单层循环,每次 \(O(1)\) 转移
- 空间复杂度:\(O(n)\),\(f\) 数组和线段端点数组
-
线性 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
浙公网安备 33010602011771号