题解: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]\) 需要处理的任务会产生影响,故一同考虑,得到转移式子:
显然这是 \(O(n^2)\) 的,可以通过弱化版,考虑优化。
我们来拆掉这个柿子:
暂时拆掉 \(\min\) 并且移个项:
显然的,在 \(i\) 一定的情况下,\(dp_i - sumt_i \times sumf_i - S \times sumf_n\) 为定值,令 \(y = dp_j,x = summf_j\):
显然的关于 \(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。

浙公网安备 33010602011771号