1 条题解
-
0
一、问题分析与数学建模
1.1 问题本质
这是一道斜率优化DP + 贪心的经典问题,核心在于如何高效地处理二次函数形式的状态转移。
问题形式化
给定:
- 个站点, 班列车
- 列车 :从站点 在时刻 出发,到站点 在时刻 到达
- 等待时间 的代价:
- 到达 的额外代价:到达时刻
目标:从站点 出发(时刻 ),到达站点 ,最小化总烦躁值。
烦躁值公式:
$$\text{Cost} = q_{s_k} + (Ap_{s_1}^2 + Bp_{s_1} + C) + \sum_{j=1}^{k-1} \left[A(p_{s_{j+1}} - q_{s_j})^2 + B(p_{s_{j+1}} - q_{s_j}) + C\right] $$
1.2 核心数学观察
观察 1:DP 状态设计
定义 :乘坐第 班列车时的最小烦躁值(不包括到达时间 )。
初始状态:虚拟一个编号为 的"列车",表示在站点 等待。
答案:
观察 2:朴素 DP 转移
对于列车 ,可以从满足以下条件的列车 转移:
- (站点连续)
- (时间可行)
朴素转移方程:
$$f[i] = \min_{j \in S_i} \left\{ f[j] + A(p_i - q_j)^2 + B(p_i - q_j) + C \right\} $$其中 。
时间复杂度:,对于 不可接受。
观察 3:转移方程的展开与变形
展开转移方程:
$$\begin{aligned} f[i] &= f[j] + A(p_i - q_j)^2 + B(p_i - q_j) + C \\ &= f[j] + Ap_i^2 - 2Ap_iq_j + Aq_j^2 + Bp_i - Bq_j + C \end{aligned} $$整理得:
$$f[i] = (Ap_i^2 + Bp_i + C) + \left[f[j] + Aq_j^2 - Bq_j - 2Ap_iq_j\right] $$关键变换:将与 有关的项分离出来。
设:
- (横坐标)
- (纵坐标)
则:
$$f[i] = (Ap_i^2 + Bp_i + C) + Y(j) - 2Ap_i \cdot X(j) $$要最小化 ,等价于最小化:
观察 4:几何意义
将每个决策点 看作平面上的点 。
要最小化 ,等价于找一条斜率为 的直线:
使得该直线经过某个决策点,并且截距 最小。
几何解释:
- 在 坐标系中,决策点形成一个点集
- 我们要找一条斜率为 的直线,从下方切过这些点
- 第一个接触到的点就是最优决策点
观察 5:凸包优化
引理 1:对于三个决策点 (按 坐标递增),如果:
$$\frac{Y(j) - Y(i)}{X(j) - X(i)} \ge \frac{Y(k) - Y(j)}{X(k) - X(j)} $$则决策点 永远不是最优决策(被 或 支配)。
证明:
- 左侧是 连线的斜率
- 右侧是 连线的斜率
- 如果左斜率 右斜率,说明 在 连线的上方或上面
- 对于任意查询斜率 :
- 如果 左斜率,选 更优
- 如果 右斜率,选 更优
- 如果左斜率 右斜率,由于左斜率 右斜率,这个区间为空或选 或 都不差
- 因此 永远不会被选中
维护下凸包:保留满足斜率递增的决策点序列。
1.3 算法框架
阶段 1:预处理
- 按列车出发时间 排序
- 初始化每个站点的决策点队列
阶段 2:动态规划
- 按排序后的顺序处理每班列车
- 对于列车 :
- 将已经到达 且 的决策点加入凸包
- 在凸包上查询斜率为 的最优决策点
- 计算
- 将 加入到站点 的待处理队列
阶段 3:统计答案
- 遍历所有到达站点 的列车
- 取
二、斜率优化DP 详解
2.1 凸包维护
数据结构:对于每个站点 ,维护一个双端队列 。
性质:
- 队列中的决策点按 坐标(即 )递增
- 相邻决策点连线的斜率递增(下凸包)
插入操作:
当新决策点 pos 到达时: while 队列长度 >= 2: 取队尾两个点 i, j if 斜率(i,j) >= 斜率(j,pos): 弹出 j(被 pos 支配) else: break 将 pos 加入队尾查询操作:
给定查询斜率 k = 2*A*p[i]: while 队列长度 >= 2: 取队头两个点 i, j if 斜率(i,j) <= k: 弹出 i(j 更优) else: break 返回队头元素
2.2 斜率比较函数
函数 1:
cmpk(i, j, k)- 判断是否删除bool cmpk(int i, int j, int k) { return (Y(j) - Y(i)) * (X(k) - X(j)) >= (Y(k) - Y(j)) * (X(j) - X(i)); }数学含义:
$$\frac{Y(j) - Y(i)}{X(j) - X(i)} \ge \frac{Y(k) - Y(j)}{X(k) - X(j)} $$即:斜率 斜率,则删除 。
注意:用乘法避免除法(防止精度问题和除零)。
函数 2:
cmpx(i, j, val)- 判断是否删除bool cmpx(int i, int j, int val) { return Y(j) - Y(i) <= val * (X(j) - X(i)); }数学含义:
即:斜率 查询斜率 ,则删除 ( 更优)。
2.3 时间顺序处理
为什么按 排序?
引理 2:按 递增顺序处理列车,查询斜率 单调递增。
证明:
- 查询斜率
- 如果 ,则 (假设 )
- 这保证了查询斜率单调,可以用单调队列优化
注意:当 时,斜率恒为 ,退化为普通 DP。
三、代码模块详解
模块 1:全局变量与数据结构
const int N = 1e5 + 10, M = 2e5 + 10; // 注意:M应该是2e5而不是1e6 int n, m, A, B, C; int x[M], y[M], p[M], q[M]; // 列车信息 int ind[N]; // 每个站点作为到达站的列车数量 int id[M]; // 列车按p[i]排序后的编号优先队列节点:
struct node { int x; // 列车编号 friend bool operator <(node x, node y) { return q[x.x] > q[y.x]; // 小根堆,按q[j]递增 } }; priority_queue<node> hp[N]; // 每个站点的待处理决策点凸包维护:
vector<int> que[N]; // 每个站点的决策点队列 int head[N], tail[N]; // 队列的头尾指针 int f[M]; // DP数组
模块 2:读入与预处理
cin >> n >> m >> A >> B >> C; for (int i = 1; i <= m; i++) { cin >> x[i] >> y[i] >> p[i] >> q[i]; ++ind[y[i]]; // 统计到达每个站点的列车数 } // 按出发时间排序 iota(id + 1, id + 1 + m, 1); // id数组初始化为1,2,...,m sort(id + 1, id + 1 + m, [&](int x, int y) { return p[x] < p[y]; }); // 初始化队列 for (int i = 1; i <= n; i++) { que[i].resize(ind[i] + 3); // 预分配空间 head[i] = 1, tail[i] = 0; // 空队列 } que[1][++tail[1]] = 0; // 站点1加入虚拟决策点0(f[0]=0, q[0]=0)关键点:
iota初始化排列ind[y[i]]用于预分配 vector 大小,避免动态扩容- 虚拟决策点 表示在站点 从时刻 开始等待
模块 3:斜率优化函数
int X(int j) { return q[j]; } int Y(int j) { return f[j] + A * q[j] * q[j] - B * q[j]; }定义坐标映射:
bool cmpk(int i, int j, int k) { return (Y(j) - Y(i)) * (X(k) - X(j)) >= (Y(k) - Y(j)) * (X(j) - X(i)); }判断斜率关系:
$$\frac{Y(j) - Y(i)}{X(j) - X(i)} \ge \frac{Y(k) - Y(j)}{X(k) - X(j)} $$返回
true表示 应该被删除(不在下凸包上)。
bool cmpx(int i, int j, int val) { return Y(j) - Y(i) <= val * (X(j) - X(i)); }判断查询斜率:
返回
true表示 应该被删除( 更优)。
模块 4:DP 主循环
for (int _ = 1; _ <= m; _++) { int i = id[_]; // 按出发时间顺序处理 // 步骤1: 将满足条件的决策点加入凸包 while (!hp[x[i]].empty() && q[hp[x[i]].top().x] <= p[i]) { int pos = hp[x[i]].top().x; // 维护下凸包:删除被支配的点 while (head[x[i]] < tail[x[i]] && cmpk(que[x[i]][tail[x[i]] - 1], que[x[i]][tail[x[i]]], pos)) { --tail[x[i]]; } que[x[i]][++tail[x[i]]] = pos; hp[x[i]].pop(); } // 步骤2: 在凸包上查询最优决策点 while (head[x[i]] < tail[x[i]] && cmpx(que[x[i]][head[x[i]]], que[x[i]][head[x[i]] + 1], 2 * A * p[i])) { ++head[x[i]]; } // 步骤3: 无可行转移 if (head[x[i]] > tail[x[i]]) { f[i] = 1e18; continue; } // 步骤4: 计算f[i] int j = que[x[i]][head[x[i]]]; int del = p[i] - q[j]; f[i] = f[j] + A * del * del + B * del + C; // 步骤5: 将i加入到达站点y[i]的待处理队列 hp[y[i]].push({i}); }算法流程:
步骤 1:插入决策点
- 从优先队列中取出所有 的决策点
- 按 递增顺序插入凸包
- 维护凸包性质(删除被支配的点)
步骤 2:查询最优决策点
- 查询斜率为 的最优决策
- 从队头删除不优的决策点
步骤 3:处理无解情况
- 如果队列为空,说明无法从站点 出发
- 设
步骤 4:计算
- 取队头元素 作为最优决策
- 计算等待时间
- 转移:
步骤 5:延迟插入
- 将 加入到达站点 的优先队列
- 等待后续从 出发的列车处理时再插入凸包
模块 5:统计答案
int ans = 1e18; for (int i = 1; i <= m; i++) { if (y[i] == n) { ans = min(ans, f[i] + q[i]); } } cout << ans << '\n';逻辑:
- 遍历所有到达站点 的列车
- 是不包括到达时间的烦躁值
- 总烦躁值 (到达时刻的额外代价)
四、正确性证明
4.1 DP 转移的正确性
定理 1:状态 正确表示乘坐第 班列车时的最小烦躁值(不含到达时间)。
证明(归纳法):
基础:(虚拟起点),正确。
归纳:假设对所有 (按处理顺序), 正确。
对于 ,由于按 递增顺序处理,所有可能转移到 的 都已经处理过。
转移方程:
$$f[i] = \min_{j \in S_i} \{f[j] + A(p_i - q_j)^2 + B(p_i - q_j) + C\} $$由归纳假设, 正确,因此 正确。
4.2 斜率优化的正确性
定理 2:凸包维护算法正确找到最优决策点。
证明:
性质 1:队列中决策点按 坐标()递增。
性质 2:相邻决策点连线斜率递增(下凸包)。
引理 3:给定查询斜率 ,最优决策点是使得连线斜率首次 的前一个点。
证明:
- 设队列为
- 斜率递增:(其中 是 到 的斜率)
- 如果 ,则 是最优决策
- 查询算法通过从队头删除 的点,正确找到
4.3 时间顺序的必要性
定理 3:必须按 递增顺序处理列车。
证明:
反例:如果不排序,可能出现 但 在 之后处理。
此时处理 时, 还未加入决策集合,可能错过最优转移。
正面论证:
- 按 排序后,处理 时,所有 的 都已处理
- 保证转移的完备性
五、复杂度分析
5.1 时间复杂度
预处理:
- 排序:
- 初始化:
DP 主循环:
- 外层循环:
- 对于每个列车 :
- 插入决策点:每个决策点最多入队出队一次,均摊
- 查询最优决策:每个决策点最多从队头弹出一次,均摊
- 计算转移:
总时间复杂度:
对于 ,约为 次操作,完全可行。
5.2 空间复杂度
- 列车信息:
- DP 数组:
- 决策点队列:(最坏情况,实际远小于此)
- 优先队列:
总空间复杂度:(均摊)
5.3 优化空间
问题:原代码
const int M = 1e6 + 10过大。优化:改为
const int M = 2e5 + 10。原因:
- 开 的数组浪费空间,可能导致栈溢出(Segmentation Fault)
六、算法优化与扩展
6.1 特殊情况的优化
情况 1:
- 烦躁值只有到达时间
- 退化为最短路问题,用 Dijkstra 或 BFS
情况 2:
- 转移方程变为线性:
- 仍需斜率优化(斜率为 )
情况 3:(单向递增)
- 可以按站点分层处理
- 简化转移逻辑
6.2 代码健壮性改进
改进 1:数组大小检查
const int M = 2e5 + 10; // 从1e6改为2e5改进 2:边界检查
if (head[x[i]] > tail[x[i]]) { f[i] = 1e18; continue; }改进 3:调试输出(可选)
#ifdef DEBUG cerr << "Processing train " << i << ": " << x[i] << "->" << y[i] << " at time " << p[i] << "->" << q[i] << endl; #endif
6.3 相关问题与扩展
-
变种问题
- 多起点多终点
- 动态添加/删除列车
- 不同的代价函数
-
一般化
- 高维凸包维护
- 在线查询(持久化数据结构)
-
理论延伸
- 凸包优化的其他应用
- 斜率优化DP的变体
七、知识点总结
7.1 核心算法技巧
-
✅ 斜率优化DP
- 将二次DP转化为凸包问题
- 维护下凸包
-
✅ 单调队列
- 维护决策点凸包
- 均摊 插入删除
-
✅ 贪心排序
- 按时间顺序处理
- 保证单调性
-
✅ 几何转化
- DP问题转化为几何问题
- 利用几何性质优化
-
✅ 优先队列
- 延迟插入决策点
- 维护时间顺序
八、总结
8.1 算法精髓
本题采用的斜率优化DP算法的核心思想:
- DP建模:定义状态 表示乘坐第 班列车的最小代价
- 数学变形:将二次转移方程转化为几何形式
- 凸包维护:用单调队列维护决策点的下凸包
- 单调性利用:按时间排序,利用查询斜率单调性优化
- 延迟插入:用优先队列管理决策点的插入时机
- 1
信息
- ID
- 4106
- 时间
- 1000ms
- 内存
- 512MiB
- 难度
- 9
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者