「SDOI2012」任务安排 3
知识点:斜率优化,二分
四 步 走 战 略
简述
给定一列 \(n\) 个有序的物品,每个物品有两个属性 \((t_i, g_i)\),给定参数 \(s\)。
要求将物品分为任意段,第 \(i\) 段 \([l_i,r_i]\) 的代价为 \(\left(is + \sum_{j=l_i}^{r_i} t_j\right)\cdot \sum_{k=l_i}^{r_i}\),要求最小化分段的代价之和。
\(1\le n\le 3\times 10^5\),\(1\le s\le 2^8\),\(|t_i|\le 2^8\),\(0\le g_i\le 2^8\)。
1S,512MB。
分析
发现分到第几段对答案有影响,设 \(f_{i,j}\) 表示将前 \(i\) 个任务分为 \(j\) 段的最小费用和,转移时枚举段数 \(k\) 和最后一段,则有:
预处理前缀和,暴力转移时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)。空间和时间都菜爆了。
发现在上述算法中必须枚举分到第几段,考虑能否优化掉状态的这一维,并优化转移。
这里用到了一种叫做「费用提前计算」的思想。发现每次转移将 \([j + 1,i]\) 这段分出后,后续元素的代价里都会加上 \(k\cdot g\),考虑在状态转移中加上这部分的影响。具体地,将状态删去一维,方程改写为如下所示:
状态转移方程很容易理解。此时已经无法准确定义 \(f\) 的含义了,但 \(f_n\) 一定表示将所有物品划分为某几段的最小代价和,且这样转移一定可以保证 \(f_n\) 的正确性。
预处理前缀和后暴力转移即可,时间复杂度 \(O(n^2)\),空间复杂度 \(O(n)\)。
但上述 \(O(n^2)\) 算法不足以通过本题,发现出现了乘积项,考虑斜率优化。记 \(st_x = \sum_{i=1}^{x} t_i\),\(sg_x = \sum_{i=1}^x g_i\),代入转移方程并略作变换:
这是一个显然的斜率优化的形式,设:
如果 \(k_i\) 与 \(x_i\) 均单调递增,套路地单调队列维护下凸包即可。总时空复杂度均为 \(O(n)\)。
但本题中可能出现 \(t_i< 0\),\(k_i\) 是不单调的,这影响了最优决策点的选择,无法使用单调队列选择最优决策点。但 \(x_i\) 单调,使用单调队列维护下凸包的做法是正确的。
仍考虑使用单调队列维护下凸包,每次查询最优决策点时在凸包上二分,找到第一个使得左侧斜率小于 \(k_i\),右侧斜率不小于 \(k_i\) 的位置即为最优决策点,不从队首弹出元素。可以发现此时的“单调队列”实际上是一个单调栈。我们实际上是在用 Andrew 算法 维护凸包。
总时间复杂度变为 \(O(n\log n)\)。
代码
//知识点:斜率优化
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
#define LD long double
const int kN = 3e5 + 10;
const LL kInf = 9e18 + 2077;
//=============================================================
int n, h = 1, t, q[kN];
LL s, ans = kInf, f[kN], sumt[kN], sumg[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmin(LL &fir, LL sec) {
if (sec < fir) fir = sec;
}
LD X(int x_) {
return sumg[x_];
}
LD Y(int x_) {
return f[x_] - s * sumg[x_];
}
LD K(int x_, int y_) {
if (X(x_) == X(y_)) return (Y(y_) > Y(x_) ? 1e18 : -1e18);
return (LD) ((Y(y_) - Y(x_)) / (X(y_) - X(x_)));
}
bool Check(LD know_, int mid_) {
return know_ <= K(q[mid_], q[mid_ + 1]);
}
int Query(int now_) {
LD know = sumt[now_];
int ret = t;
for (int l = h, r = t - 1; l <= r; ) {
int mid = (l + r) >> 1;
if (Check(know, mid)) {
ret = mid; //q[ret] 是最靠右的使得 check 为 true 的位置,即 q[ret] 左侧斜率小于 k,右侧斜率不小于 k。
r = mid - 1;
} else {
l = mid + 1;
}
}
return q[ret];
}
void Insert(int now_) {
while (h < t && K(q[t - 1], q[t]) >= K(q[t - 1], now_)) -- t;
q[++ t] = now_;
}
//=============================================================
int main() {
n = read(), s = read();
for (int i = 1; i <= n; ++ i) {
sumt[i] = sumt[i - 1] + read();
sumg[i] = sumg[i - 1] + read();
}
Insert(0);
for (int i = 1; i <= n; ++ i) {
int j = Query(i);
f[i] = f[j] + sumt[i] * (sumg[i] - sumg[j]) +
s * (sumg[n] - sumg[j]);
Insert(i);
}
printf("%lld\n", f[n]);
return 0;
}