题目链接:https://www.luogu.com.cn/problem/CF311B
题目描述
小S是农场主,他养了 \(M\) 只猫,雇了 \(P\) 位饲养员。农场中有一条笔直的路,路边有 \(N\) 座山,从 \(1\) 到 \(N\) 编号。第 \(i\) 座山与第 \(i-1\) 座山之间的距离是 \(D_i\)。饲养员都住在 \(1\) 号山。
有一天,猫们出去玩。第 \(i\) 只猫去 \(H_i\) 号山玩,玩到时刻 \(T_i\) 停止,然后再原地等待饲养员来接。饲养员们必须回收所有的猫。每个饲养员沿着路从 \(1\) 号山走到 \(N\) 号山,把各座山上已经在等待的猫全部接走。饲养员在路上行走需要时间,速度为 \(1\) 米/单位时间。饲养员在每座山上接猫的时间可以忽略,可以携带的猫的数量为无穷大。
例如,有两座相距为 \(1\) 的山,一只猫在 \(2\) 号山玩,玩到时刻 \(3\) 开始等待。如果饲养员从 \(1\) 号山在时刻 \(2\) 或 \(3\) 出发,那么他可以接到猫,猫的等待时间为 \(0\) 或 \(1\)。而如果他于时刻 \(1\) 出发,那么他将于时刻 \(2\) 经过 \(2\) 号山,不能接到当时仍在玩的猫。
你的任务是规划每个饲养员从 \(1\) 号出发的时间,使得所有猫等待时间的总和尽量小。饲养员出发的时间可以为负。
输入格式
第一行三个整数 \(N,M,P\)。
第二行 \(n-1\) 个正整数 \(D_i\) ,分别表示 \(D_2 \sim D_N\)(\(D_i\) 表示第 \(i\) 座山与第 \(i-1\) 座山之间的距离是 \(D_i\))。
接下来 \(M\) 行,每行包含两个整数 \(H_i, T_i\),分别表示第 \(i\) 只猫所在的山的编号和结束玩的时刻。
输出格式
输出一个整数表示答案。
样例输入
4 6 2
1 3 5
1 0
2 1
4 9
1 10
2 10
3 12
样例输出
3
数据规模
对于100%的数据,保证 \(2 \le N \le 10^5, 1 \le M \le 10^5, 1 \le P \le 100\)
问题分析
对于每只猫,设
一名饲养员如果想要接到第 \(i\) 只猫,就必须在 \(A_i\) 时刻及之后的时间从 \(1\) 号山出发。若出发时间为 \(t\) ,则这只猫的等待时间就是 \(t - A_i\)。
把 \(A_i\) 从小到大排序,求出排好序的 \(A\) 数组的前缀和,记录在数组 \(S\) 中。根据贪心策略,每个饲养员带走的猫一定是按照 \(A\) 排序后连续的若干只。
此时,本题就与“ 任务安排 ”非常类似。饲养员就是“机器”,猫就是“任务”,每个饲养员带走连续的一段猫。设 \(F[i,j]\) 表示前 \(i\) 个饲养员带走前 \(j\) 只猫,猫等待时间的总和最小是多少。
假设第 \(i\) 个饲养员带走第 \(k+1 \sim j\) 只猫,那么该饲养员的最早出发时间就是 \(A_j\)。这些猫的等待时间之和就是
得到状态转移方程为:
直接枚举 \(k\),求出 \(F[i,j]\) 的最小值,时间复杂度为 \(O(P \cdot M^2)\)。
实现代码如下(TLE):
#include
using namespace std;
const int maxp = 110, maxn = 100010;
int n, m, p, H;
long long D[maxn], T, sumD[maxn], a[maxn], sum[maxn], f[maxp][maxn], ans = -1;
int main() {
cin >> n >> m >> p;
for (int i = 2; i <= n; i ++) {
cin >> D[i];
sumD[i] = sumD[i-1] + D[i];
}
for (int i = 1; i <= m; i ++) {
cin >> H >> T;
a[i] = T - sumD[H];
}
sort(a+1, a+1+m);
for (int i = 1; i <= m; i ++) sum[i] = sum[i-1] + a[i];
memset(f, 0x3f, sizeof(f));
f[1][0] = 0;
for (int i = 1; i <= p; i ++) {
for (int j = 1; j <= m; j ++) {
if (i == 1) {
f[i][j] = a[j] * j - sum[j];
continue;
}
for (int k = 0; k < j; k ++) {
assert(f[i-1][k] != 0x3f3f3f3fLL);
long long tmp = f[i-1][k] + a[j] * (j - k) - (sum[j] - sum[k]);
if (f[i][j] > tmp)
f[i][j] = tmp;
}
}
}
for (int i = 1; i <= p; i ++) {
if (ans == -1 || ans > f[i][m]) ans = f[i][m];
}
cout << ans << endl;
return 0;
}
把外侧循环 \(i\) 看做定值,\(j\) 是状态变量,\(k\) 是决策变量。方程中存在乘积项 \(A_j \times k\),应考虑使用斜率优化。
去掉 \(\min\) 函数,对方程进行移项,等号左边放仅与 \(k\) 有关的项,等号右边放 \(j,k\) 乘积项以及仅与 \(j\) 有关的项:
以 \(k\) 为横坐标, \(F[i-1,k] + S_k\) 为纵坐标建立平面直角坐标系。上式是一条以 \(A_j\) 为斜率,\(F[i,j] - A_j \times j + S_j\) 为截距的直线,当截距最小化时, \(F[i,j]\) 取到最小值。
在最小化截距的线性规划问题中,应维护一个下凸壳。建立一个单调队列,队列中相邻两个决策 \(k_1\) 和 \(k_2\) 应满足 \(k_1 \lt k_2\) 并且斜率
单调递增。
因为直线斜率 \(A_j\) 也已经从小到大排过序,所以在程序实现中,采用与“任务安排”中类似的单调队列的三个基本操作即可(因为斜率 \(A_j\) 是单调递增的,所以我们不需要在队列中进行二分,而只需要去除队首斜率小于 \(A_j\) 的即可)。
实现代码如下:
#include
using namespace std;
const int maxp = 110, maxn = 100010;
int n, m, p, H, q[maxn], l, r;
long long D[maxn], T, sumD[maxn], a[maxn], sum[maxn], f[maxp][maxn], g[maxn], ans = -1;
int main() {
cin >> n >> m >> p;
for (int i = 2; i <= n; i ++) {
cin >> D[i];
sumD[i] = sumD[i-1] + D[i];
}
for (int i = 1; i <= m; i ++) {
cin >> H >> T;
a[i] = T - sumD[H];
}
sort(a+1, a+1+m);
for (int i = 1; i <= m; i ++) sum[i] = sum[i-1] + a[i];
memset(f, 0x3f, sizeof(f));
f[1][0] = 0;
for (int i = 1; i <= p; i ++) {
if (i == 1) {
for (int j = 1; j <= m; j ++) f[i][j] = a[j] * j - sum[j];
}
else {
for (int j = 1; j <= m; j ++) g[j] = f[i-1][j] + sum[j];
q[l = r = 1] = 0;
for (int j = 1; j <= m; j ++) {
while (l < r && g[q[l+1]] - g[q[l]] <= a[j] * (q[l+1] - q[l])) l ++;
f[i][j] = min(f[i-1][j], g[q[l]] + a[j] * (j - q[l]) - sum[j]);
if (g[j] >= 0x3f3f3f3f3f3f3f3fLL) continue;
while (l < r && (g[j] - g[q[r]]) * (q[r] - q[r-1]) <= (g[q[r]] - g[q[r-1]]) * (j - q[r])) r --;
q[++r] = j;
}
}
}
cout << f[p][m] << endl;
return 0;
}