把每个站点上的换乘 DP 写成关于发车时刻 p 的直线最小值查询,再按时间扫描并为每个站维护单调队列凸包。
OJ: luogu
题目 ID: P6302
难度:省选/NOI-
标签:动态规划斜率优化凸包优化按时间扫描
日期: 2026-06-21 07:23
题意
有很多班列车,每班车固定:
- 在站点
x_i的时刻p_i发车 - 在站点
y_i的时刻q_i到站
小猫从时刻 0 开始待在 1 号站,目标是最终到 n 号站。
等待 t 个时刻的代价是:
A t^2 + B t + C
最后到达终点时,还要再加上最终到达时刻。
思路
先看最直接的暴力转移:
#include <bits/stdc++.h>
using namespace std;
const long long INF = (1LL << 62);
const int MAXM = 5005;
struct Train {
int x, y, p, q;
} tr[MAXM];
int n, m;
long long A, B, C;
long long dp[MAXM];
long long wait_cost(long long t) {
return A * t * t + B * t + C;
}
bool cmp_train(const Train &lhs, const Train &rhs) {
if (lhs.q != rhs.q) {
return lhs.q < rhs.q;
}
if (lhs.p != rhs.p) {
return lhs.p < rhs.p;
}
if (lhs.x != rhs.x) {
return lhs.x < rhs.x;
}
return lhs.y < rhs.y;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// brute.cpp:小数据暴力 DP。
// 按到达时间排序,枚举所有能接上的前一班车。
cin >> n >> m >> A >> B >> C;
for (int i = 1; i <= m; i++) {
cin >> tr[i].x >> tr[i].y >> tr[i].p >> tr[i].q;
}
sort(tr + 1, tr + m + 1, cmp_train);
long long answer = INF;
for (int i = 1; i <= m; i++) {
dp[i] = INF;
if (tr[i].x == 1) {
dp[i] = min(dp[i], wait_cost(tr[i].p));
}
for (int j = 1; j < i; j++) {
if (dp[j] >= INF / 2) {
continue;
}
if (tr[j].y == tr[i].x && tr[j].q <= tr[i].p) {
dp[i] = min(dp[i], dp[j] + wait_cost(tr[i].p - tr[j].q));
}
}
if (tr[i].y == n) {
answer = min(answer, dp[i] + tr[i].q);
}
}
cout << answer << '\n';
return 0;
}设 dp[i] 表示最后乘坐第 i 班列车时的最小烦躁值。
如果列车 j 能接到列车 i,那么:
dp[i] = min(dp[j] + A(p_i-q_j)^2 + B(p_i-q_j) + C)
若它是第一班车,还要考虑:
dp[i] = A p_i^2 + B p_i + C
把转移展开:
dp[i] = A p_i^2 + B p_i + C + min(dp[j] + A q_j^2 - B q_j - 2A q_j p_i)
对于固定的前驱 j,后半部分是关于 p_i 的一次函数。
所以可以对每个站点维护一个凸包:
- 一条线对应一种“已经到达这个站”的方案
- 之后若有列车从这个站发车,只要在
p_i处查询最小值
又因为:
- 查询时间
p_i按扫描顺序单调不减 - 插入时间
q_j也单调不减,所以斜率单调
于是每个站点都可以用单调队列维护下凸壳。
主流程按时间从小到大扫描:
- 先处理当前时刻到达的列车,把它们对应的直线插入终点站
- 再处理当前时刻出发的列车,在起点站的凸壳上查询
这样自然支持 q_j = p_i 的零等待换乘。
DP 转移方程
核心状态:
dp[i] 为最后乘第 i 班车的最小烦躁值
核心转移:
dp[i]=A p_i^2+B p_i+C+min(dp[j]+Aq_j^2-Bq_j-2Aq_j p_i)
答案收束:
到终点列车 dp[i]+q_i 取最小
代码
#include <bits/stdc++.h>
using namespace std;
const long long INF = (1LL << 62);
const int MAXN = 100005;
const int MAXM = 1000005;
struct Train {
int x, y, p, q;
} tr[MAXM];
struct Line {
long long k, b;
};
int n, m;
long long A, B, C;
long long dp[MAXM];
vector<int> depart_at[40005];
vector<int> arrive_at[40005];
// 每个站维护一个下凸壳,表示已经到达该站的所有方案。
struct Hull {
vector<Line> q;
int head = 0;
void clear() {
q.clear();
head = 0;
}
bool empty() const {
return head >= (int) q.size();
}
long long value(const Line &line, long long x) const {
return line.k * x + line.b;
}
// 下凸壳判劣。
bool bad(const Line &a, const Line &b, const Line &c) const {
__int128 left = (__int128) (b.b - a.b) * (b.k - c.k);
__int128 right = (__int128) (c.b - b.b) * (a.k - b.k);
return left >= right;
}
void add_line(long long k, long long b) {
Line line;
line.k = k;
line.b = b;
if (!q.empty() && q.back().k == line.k) {
if (q.back().b <= line.b) {
return;
}
q.pop_back();
if (head > (int) q.size()) {
head = (int) q.size();
}
}
while ((int) q.size() - head >= 2 && bad(q[(int) q.size() - 2], q[(int) q.size() - 1], line)) {
q.pop_back();
}
q.push_back(line);
}
long long query(long long x) {
while ((int) q.size() - head >= 2 && value(q[head], x) >= value(q[head + 1], x)) {
head++;
}
return value(q[head], x);
}
};
Hull station_hull[MAXN];
long long wait_cost(long long t) {
return A * t * t + B * t + C;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> A >> B >> C;
for (int i = 1; i <= m; i++) {
cin >> tr[i].x >> tr[i].y >> tr[i].p >> tr[i].q;
depart_at[tr[i].p].push_back(i);
arrive_at[tr[i].q].push_back(i);
}
// 起点站 1 在时刻 0 就已经可以开始等待。
// 若下一班车在时刻 p 发车,那么代价就是:
// A p^2 + B p + C
station_hull[1].add_line(-2LL * A * 0, 0);
long long answer = INF;
for (int t = 0; t <= 40000; t++) {
// 先处理这个时刻到达的列车,因为 q_u <= p_v,允许到站后立刻换乘同一时刻发车的车。
for (unsigned int idx = 0; idx < arrive_at[t].size(); idx++) {
int id = arrive_at[t][idx];
if (dp[id] >= INF / 2) {
continue;
}
int y = tr[id].y;
int q = tr[id].q;
// 若之后在时刻 p >= q 从这个站继续走,
// 新增等待代价是 A(p-q)^2 + B(p-q) + C。
// 展开后关于 p 的部分为:
// A p^2 + B p + C + (-2Aq) * p + (dp + Aq^2 - Bq)
// 所以插入一条斜率 -2Aq 的直线。
long long k = -2LL * A * q;
long long b = dp[id] + A * 1LL * q * q - B * 1LL * q;
station_hull[y].add_line(k, b);
}
for (unsigned int idx = 0; idx < depart_at[t].size(); idx++) {
int id = depart_at[t][idx];
int x = tr[id].x;
int p = tr[id].p;
dp[id] = INF;
if (station_hull[x].empty()) {
continue;
}
long long best = station_hull[x].query(p);
dp[id] = A * 1LL * p * p + B * 1LL * p + C + best;
if (tr[id].y == n) {
answer = min(answer, dp[id] + tr[id].q);
}
}
}
cout << answer << '\n';
return 0;
}复杂度
时间复杂度
总结
这题看起来是图上最短路,实际上更像“按站点分组的 DP”。
关键是把等待代价展开成:
- 当前列车固定的二次项
- 前驱列车形成的直线项
这样才能把枚举前驱优化成凸包查询。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
