P2365 任务安排
知识点:斜率优化
原题面:Luogu
三 步 走 战 略
简述
给定一列 \(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 5000\),\(0\le s\le 50\),\(1\le t_i,g_i\le 100\)。
1S,128MB。
分析
以下将会给出三种做法。
\(O(n^3)\)
发现分到第几段对答案有影响,设 \(f_{i,j}\) 表示将前 \(i\) 个任务分为 \(j\) 段的最小费用和,转移时枚举段数 \(k\) 和最后一段,则有:
预处理前缀和,暴力转移时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)。空间和时间都菜爆了。
\(O(n^2)\)
发现在上述算法中必须枚举分到第几段,考虑能否优化掉状态的这一维,并优化转移。
这里用到了一种叫做「费用提前计算」的思想。发现每次转移将 \([j + 1,i]\) 这段分出后,后续元素的代价里都会加上 \(k\cdot g\),考虑在状态转移中加上这部分的影响。具体地,将状态删去一维,方程改写为如下所示:
状态转移方程很容易理解。此时已经无法准确定义 \(f\) 的含义了,但 \(f_n\) 一定表示将所有物品划分为某几段的最小代价和,且这样转移一定可以保证 \(f_n\) 的正确性。
预处理前缀和后暴力转移即可,时间复杂度 \(O(n^2)\),空间复杂度 \(O(n)\)。
\(O(n)\)
上述 \(O(n^2)\) 算法已经可以通过原题了,但这还不够。
记 \(st_x = \sum_{i=1}^{x} t_i\),\(sg_x = \sum_{i=1}^x g_i\),代入转移方程并略作变换:
这是一个显然的斜率优化的形式,设:
由于 \(0\le t_i,g_i\),\(k_i\) 与 \(x_i\) 均单调递增,套路地单调队列维护下凸包即可。
总时空复杂度均为 \(O(n)\)。
代码
\(O(n)\)
//知识点:斜率优化,费用提前计算
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
#define LD long double
const int kN = 5e3 + 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_)));
}
int Query(int now_) {
LD know = sumt[now_];
while (h < t && K(q[h], q[h + 1]) <= know) ++ h;
return q[h];
}
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;
}
\(O(n^2)\)
//知识点:DP,费用提前计算
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 5e3 + 10;
const LL kInf = 9e18 + 2077;
//=============================================================
int n, s;
LL 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;
}
LL Calc(int l_, int r_) {
LL t = sumt[r_];
LL g = sumg[r_] - sumg[l_ - 1];
return t * g + 1ll * s * (sumg[n] - sumg[l_ - 1]);
}
//=============================================================
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();
}
memset(f, 63, sizeof (f));
f[0] = 0;
for (int i = 1; i <= n; ++ i) {
for (int j = 0; j < i; ++ j) {
Chkmin(f[i], f[j] + Calc(j + 1, i));
}
}
printf("%lld\n", f[n]);
return 0;
}
\(O(n^3)\)
为了不 MLE 开了 int = =
会炸的(
//知识点:DP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
#define LD long double
const int kN = 5e3 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n, s;
int ans = kInf, f[kN][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(int &fir, int sec) {
if (sec < fir) fir = sec;
}
int Calc(int k_, int l_, int r_) {
int t = k_ * s + sumt[r_];
int g = sumg[r_] - sumg[l_ - 1];
return t * g;
}
//=============================================================
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();
}
memset(f, 63, sizeof (f));
f[0][0] = 0;
for (int k = 1; k <= n; ++ k) {
for (int i = k; i <= n; ++ i) {
for (int j = 0; j < i; ++ j) {
Chkmin(f[i][k], f[j][k - 1] + Calc(k, j + 1, i));
}
}
}
for (int i = 1; i <= n; ++ i) Chkmin(ans, f[n][i]);
printf("%d\n", ans);
return 0;
}