把家庭按最终去的会场分成 4 段,设 dp[k][i] 表示前 i 户用了 k 个中间会场的最小代价,再把区间代价整理成直线形式,用斜率优化把 3 层转移压到线性。
OJ: luogu
题目 ID: P8632
难度:提高+/省选-
标签:动态规划斜率优化前缀和优化
日期: 2026-06-21 06:52
题意
公路上有 n 户家庭,第 i 户在位置 d_i,人数是 t_i。
要设置 4 个集会点:
p1, p2, p3在公路中间p4 = L固定在公路终点
并满足:
p1 <= p2 <= p3 <= p4
每户家庭只能向右走,去参加离自己最近且不在左边的那个集会点。
某户家庭的总代价是:
人数 * 行走距离
要求总开销最小。
思路
先看一个适合小数据验证的朴素 DP:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205;
const long long INF = (1LL << 60);
int n, L;
int d[MAXN];
int t[MAXN];
long long pre_w[MAXN];
long long pre_dw[MAXN];
long long dp[5][MAXN];
long long seg_cost(int l, int r) {
if (l > r) {
return 0;
}
long long sum_w = pre_w[r] - pre_w[l - 1];
long long sum_dw = pre_dw[r] - pre_dw[l - 1];
return 1LL * d[r] * sum_w - sum_dw;
}
long long last_cost(int l) {
if (l > n) {
return 0;
}
long long sum_w = pre_w[n] - pre_w[l - 1];
long long sum_dw = pre_dw[n] - pre_dw[l - 1];
return 1LL * L * sum_w - sum_dw;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// brute.cpp:按“前 i 户用了 k 个中间会场”的朴素 DP。
cin >> n >> L;
for (int i = 1; i <= n; i++) {
cin >> d[i] >> t[i];
pre_w[i] = pre_w[i - 1] + t[i];
pre_dw[i] = pre_dw[i - 1] + 1LL * d[i] * t[i];
}
for (int k = 0; k <= 3; k++) {
for (int i = 0; i <= n; i++) {
dp[k][i] = INF;
}
}
dp[0][0] = 0;
long long ans = last_cost(1);
for (int k = 1; k <= 3; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
if (dp[k - 1][j] >= INF / 2) {
continue;
}
dp[k][i] = min(dp[k][i], dp[k - 1][j] + seg_cost(j + 1, i));
}
ans = min(ans, dp[k][i] + last_cost(i + 1));
}
}
cout << ans << '\n';
return 0;
}因为所有家庭都只能向右走,所以一旦会场位置确定,每户家庭最终去的会场一定按顺序分成连续几段。
也就是说:
- 前一段去
p1 - 下一段去
p2 - 再下一段去
p3 - 最后一段去终点
L
所以本质上是把家庭分成 4 段。
如果某一段家庭 [l..r] 都去位置 d_r 这个会场,那么这段的总代价是:
sum( (d_r - d_i) * t_i )
用前缀和可以写成:
d_r * (sum t_i) - (sum d_i * t_i)
于是设:
dp[k][i] 表示前 i 户家庭已经用掉 k 个中间会场时的最小代价
这里 k = 0..3,终点 L 那个会场单独最后处理。
转移时:
dp[k][i] = min( dp[k-1][j] + cost(j+1, i) )
其中 j < i。
把 cost(j+1, i) 展开并整理:
dp[k][i] = d_i * pre_w[i] - pre_dw[i] + min( dp[k-1][j] + pre_dw[j] - d_i * pre_w[j] )
对固定的 j 来说,括号里的部分是一条关于 d_i 的直线:
(dp[k-1][j] + pre_dw[j]) - pre_w[j] * d_i
于是每一层 k 都可以做一次斜率优化。
最后,前 i 户用掉了 1 到 3 个中间会场后,剩下 [i+1..n] 这段全部去终点 L,把这部分代价再加上即可。
这就把原来的
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const long long INF = (1LL << 62);
int n, L;
int d[MAXN];
int t[MAXN];
long long pre_w[MAXN];
long long pre_dw[MAXN];
long long dp_prev[MAXN], dp_cur[MAXN];
int que[MAXN];
// 把区间 [l..r] 的家庭都安排到 d[r] 这个会场,总代价是多少。
long long seg_cost(int l, int r) {
if (l > r) {
return 0;
}
long long sum_w = pre_w[r] - pre_w[l - 1];
long long sum_dw = pre_dw[r] - pre_dw[l - 1];
return 1LL * d[r] * sum_w - sum_dw;
}
// 把区间 [l..n] 的家庭都安排到终点 L,总代价是多少。
long long last_cost(int l) {
if (l > n) {
return 0;
}
long long sum_w = pre_w[n] - pre_w[l - 1];
long long sum_dw = pre_dw[n] - pre_dw[l - 1];
return 1LL * L * sum_w - sum_dw;
}
long long line_value(int j, int x) {
return dp_prev[j] + pre_dw[j] - 1LL * x * pre_w[j];
}
bool is_bad(int a, int b, int c) {
long long m1 = -pre_w[a];
long long m2 = -pre_w[b];
long long m3 = -pre_w[c];
long long b1 = dp_prev[a] + pre_dw[a];
long long b2 = dp_prev[b] + pre_dw[b];
long long b3 = dp_prev[c] + pre_dw[c];
return (__int128)(b2 - b1) * (m2 - m3) >= (__int128)(b3 - b2) * (m1 - m2);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> L;
for (int i = 1; i <= n; i++) {
cin >> d[i] >> t[i];
pre_w[i] = pre_w[i - 1] + t[i];
pre_dw[i] = pre_dw[i - 1] + 1LL * d[i] * t[i];
}
for (int i = 0; i <= n; i++) {
dp_prev[i] = INF;
}
dp_prev[0] = 0;
long long ans = last_cost(1);
// 最多再额外放 3 个中间会场。
for (int k = 1; k <= 3; k++) {
for (int i = 0; i <= n; i++) {
dp_cur[i] = INF;
}
int head = 1, tail = 0;
if (dp_prev[0] < INF / 2) {
que[++tail] = 0;
}
for (int i = 1; i <= n; i++) {
// 当前层转移到 i 时,只能从 j < i 转过来,
// 因此先查询,再把 j=i 加入队列供后面使用。
while (head < tail && line_value(que[head], d[i]) >= line_value(que[head + 1], d[i])) {
head++;
}
if (head <= tail) {
dp_cur[i] = 1LL * d[i] * pre_w[i] - pre_dw[i] + line_value(que[head], d[i]);
}
if (dp_prev[i] < INF / 2) {
while (head <= tail && pre_w[que[tail]] == pre_w[i]) {
if (dp_prev[que[tail]] + pre_dw[que[tail]] <= dp_prev[i] + pre_dw[i]) {
goto skip_insert;
}
tail--;
}
while (head < tail && is_bad(que[tail - 1], que[tail], i)) {
tail--;
}
que[++tail] = i;
}
skip_insert:
;
}
for (int i = 1; i <= n; i++) {
if (dp_cur[i] < INF / 2) {
ans = min(ans, dp_cur[i] + last_cost(i + 1));
}
}
for (int i = 0; i <= n; i++) {
dp_prev[i] = dp_cur[i];
}
}
cout << ans << '\n';
return 0;
}复杂度
时间复杂度
总结
这题的关键是先看出“只能向右走”意味着答案一定是按顺序分段。
一旦把问题变成“区间分段 DP”,再把区间代价整理成:
- 一个只和
i有关的项 - 加上一条来自
j的直线
斜率优化就很自然了。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
