题解:P10979 任务安排 2

传送门

Solution

首先考虑 \(O(n^2)\) 的暴力 dp,我们记 \(dp_i\) 表示已经处理完前 \(i\) 个任务的最小花费,枚举前 \(i\) 个中分批于 \(j\) 位置,对于处理任务的花费,我们可以记 \(sumt_i,sumf_i\) 两个前缀和,花费即为 \(sumt_i \times (sumf_i - sumf_j)\),对于启动机器的时间,显然它对后面 \([j + 1.n]\) 需要处理的任务会产生影响,故一同考虑,得到转移式子:

\[dp_{i} = \min_{j=0}^{i-1}{dp_{j} + sumt_i \times (sumf_i - sumf_j) + S \times (sumf_n - sumf_j)} \]

显然这是 \(O(n^2)\) 的,可以通过弱化版,考虑优化。

我们来拆掉这个柿子:

\[dp_{i} = \min_{j=0}^{i-1}{dp_{j} + sumt_i \times sumf_i -sumt_i \times sumf_j + S \times sumf_n - S \times sumf_j} \]

暂时拆掉 \(\min\) 并且移个项:

\[dp_j = sumf_j \times (sumt_i + S) + dp_i - sumt_i \times sumf_i - S \times sumf_n \]

显然的,在 \(i\) 一定的情况下,\(dp_i - sumt_i \times sumf_i - S \times sumf_n\) 为定值,令 \(y = dp_j,x = summf_j\)

\[y = x \times (sumt_i + S) + dp_i - sumt_i \times sumf_i - S \times sumf_n \]

显然的关于 \(x\) 的一次函数,斜率为 \(k = (sumt_i + S)\),截距为 \(b = dp_i - sumt_i \times sumf_i - S \times sumf_n\)

所以说建立平面直角坐标系,决策点集中每个点为 \((sumf_j,dp_j)\),我们现在就将题意转化成了对于一条直线 \(y' = kx + b'\),其中 \(b' \in (-\infty,\infty)\),使得它与决策点集中的点相交后 \(b'\) 计算出来最小。

如下图:

红色图像为 \(y'\),蓝色图像为它与 \(C\) 所交。

显然 \(C\) 点为最优决策点,且所有最优决策点定位于下凸壳。

考虑如何维护,对于直线 \(l_{p_ip_{i-1}}\) 和直线 \(l_{p_ip_{i+1}}\),在满足 \(k_{l_{p_ip_{i-1}}} < k <k_{l_{p_ip_{i+1}}}\) 时点 \(p_i\) 为最优决策点,故选用具有单调性的容器(此处使用单调队列)进行操作,每次二分找到满足该条件的点,然后插入当前决策点 \(i\),检查 \(i\) 是否与上一个点构成下凸壳,否则就踢掉队尾直到符合下凸壳,即为保证新加入的直线的斜率大于队尾直线的斜率,每次插入新决策点前 dp 转移即可,复杂度 \(O(n \log n)\)

但是其实有更优做法,所以该题为 P5785 弱化版。

由于本题 \(t_i,c_i\) 满足为正整数,斜率和截距单增,因此下凸壳加入新决策点一定是不断向右加入,只需要在转移前在单调队列中去除无用策略,因为凸包最左侧的点一定是最优的,所以对于当前的 \(k\),判断队头直线的斜率,不断踢出队头直到找到第一个大于 \(k\) 的队头作为最优决策点转移,这个操作均摊 \(O(1)\),故总复杂度 \(O(n)\)

Code

\(O(n \log n)\) 做法。

#include <bits/stdc++.h>
#define int long long

using namespace std;
inline int read() {
    int res = 0, f = 1;
    char ch = getchar();
    while (!isdigit (ch)) f = ch == '-' ? -1 : 1, ch = getchar();
    while (isdigit (ch)) res = (res << 1) + (res << 3) + (ch ^ 48), ch = getchar();
    return res * f;
}
const int MAXN = 3e5 + 10;
int n, S, dp[MAXN], t[MAXN], f[MAXN], preSumt[MAXN], preSumf[MAXN], q[MAXN], head, tail;

signed main() {
    n = read(), S = read();
    for (int i = 1; i <= n; i ++)
        t[i] = read(), f[i] = read();
    for (int i = 1; i <= n; i ++) {
        preSumt[i] = preSumt[i - 1] + t[i];
        preSumf[i] = preSumf[i - 1] + f[i];
    }
    for (int i = 1; i <= n; i ++) {
        int l = head, r = tail, mid = 0;
        while (l < r) {
            mid = l + r >> 1;
            if (dp[q[mid + 1]] - dp[q[mid]] >= (preSumf[q[mid + 1]] - preSumf[q[mid]]) * (preSumt[i] + S)) r = mid;
            else l = mid + 1;
        }
        dp[i] = dp[q[l]] - (S + preSumt[i]) * preSumf[q[l]] + preSumt[i] * preSumf[i] + S * preSumf[n];
        while (head < tail && (__int128_t)(dp[q[tail]] - dp[q[tail - 1]]) * (preSumf[i] - preSumf[q[tail]]) >= (__int128_t)(dp[i] - dp[q[tail]]) * (preSumf[q[tail]] - preSumf[q[tail - 1]])) tail --;
        q[++ tail] = i;
    }
    printf ("%lld\n", dp[n]);
    return 0;
}

\(O(n)\) 做法。

#include <bits/stdc++.h>
#define int long long

using namespace std;
inline int read() {
    int res = 0, f = 1;
    char ch = getchar();
    while (!isdigit (ch)) f = ch == '-' ? -1 : 1, ch = getchar();
    while (isdigit (ch)) res = (res << 1) + (res << 3) + (ch ^ 48), ch = getchar();
    return res * f;
}
const int MAXN = 3e5 + 10;
int n, S, dp[MAXN], t[MAXN], f[MAXN], preSumt[MAXN], preSumf[MAXN], q[MAXN], head, tail;

signed main() {
    n = read(), S = read();
    for (int i = 1; i <= n; i ++)
        t[i] = read(), f[i] = read();
    for (int i = 1; i <= n; i ++) {
        preSumt[i] = preSumt[i - 1] + t[i];
        preSumf[i] = preSumf[i - 1] + f[i];
    }
    for (int i = 1; i <= n; i ++) {
        while (head < tail && (__int128_t)(dp[q[head + 1]] - dp[q[head]]) <= (__int128_t)(preSumf[q[head + 1]] - preSumf[q[head]]) * (preSumt[i] + S)) head ++;
        dp[i] = dp[q[head]] - (S + preSumt[i]) * preSumf[q[head]] + preSumt[i] * preSumf[i] + S * preSumf[n];
        while (head < tail && (__int128_t)(dp[q[tail]] - dp[q[tail - 1]]) * (preSumf[i] - preSumf[q[tail]]) >= (__int128_t)(dp[i] - dp[q[tail]]) * (preSumf[q[tail]] - preSumf[q[tail - 1]])) tail --;
        q[++ tail] = i;
    }
    printf ("%lld\n", dp[n]);
    return 0;
}

Warning

计算斜率可以使用交叉相乘规避掉精度问题,但是有可能暴 longlong 需要 int128。

posted @ 2025-01-10 09:12  xAlec  阅读(13)  评论(0)    收藏  举报