CF311B Cats Transport 题解 斜率优化DP

题目链接: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\)

问题分析

对于每只猫,设

\[A_i = T_i - \sum_{1 \le j \le H_i} D_j \]

一名饲养员如果想要接到第 \(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\)。这些猫的等待时间之和就是

\[\sum_{k \lt p \le j} (A_j - A_p) = A_j \times (j-k) - (S_j - S_k) \]

得到状态转移方程为:

\[F[i,j] = \min_{0 \le k \lt j} \{ F[i-1, k] + A_j \times (j - k) - (S_j - S_k) \} \]

直接枚举 \(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\) 有关的项:

\[F[i-1,k] + S_k = A_j \times k + F[i,j] - A_j \times j + S_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\) 并且斜率

\[\frac{(F[i-1,k_2] + S_{k_2}) - (F[i-1, k_1] + S_{k_1})}{k_2 - k_1} \]

单调递增。

因为直线斜率 \(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;
}

你可能感兴趣的:(CF311B Cats Transport 题解 斜率优化DP)