一个悲惨的前言
斜率优化dp 又称为 convex hull trick
引子:任务安排1(ACWing 300)
有 N 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。
机器会把这 N 个任务分成若干批,每一批包含连续的若干个任务。
从时刻 0 开始,任务被分批加工,执行第 i 个任务所需的时间是 Ti。
另外,在每批任务开始前,机器需要 S 的启动时间,故执行一批任务所需的时间是启动时间 S 加上每个任务所需时间之和。
一个任务执行后,将在机器中稍作等待,直至该批任务全部执行完毕。
也就是说,同一批任务将在同一时刻完成。
每个任务的费用是它的完成时刻乘以一个费用系数 Ci。
请为机器规划一个分组方案,使得总费用最小。
输入格式
第一行包含整数 N。
第二行包含整数 S。
接下来 N 行每行有一对整数,分别为 Ti 和 Ci,表示第 i 个任务单独完成所需的时间 Ti 及其费用系数 Ci。
输出格式
输出一个整数,表示最小总费用。
数据范围
1≤N≤5000,
0≤S≤50,
1≤Ti,Ci≤100
输入样例:
5
1
1 3
3 2
4 3
2 3
1 4
输出样例:
153
题目大意:给定n个任务,顺序固定,每个任务执行需要ti的
时间,但每次划分需要在序列中插入s的启动时间,每个任务
的花费是他的 完成时间*费用系数ti.
求花费最小。
化简思想:每次启动的额外花费其实是可以提出表达式外独
立计算的,所以只需要在每次划分是直接加上s的持续影响,
这样在后面就不需要考虑s了。
假设划分的上一段结尾是 j ,那么s的影响为
s*c[j+1] + s*c[j+2] ... + s*c[n-1] + s*c[n]
预处理出c的前缀和sumc
那么即为 s*(sumc[n] - sumc[j])
而本次划分的花费为sumt[i]*(sumc[i]-sumc[j])
即共计为 Sum =
sumt[i]*(sumc[i]-sumc[j])+s*(sumc[n]-sumc[j])
f[i]表示将前i个任务分成若干批执行的最小费用
引入中间量j, 即将前j个任务划分为若干组的最小费用为f[j]
将j+1....n划分为一组费用为 Sum
那么转移方程即为:
f[i] = min{f[j] + Sum}
= min{f[j] + sumt[i]*(sumc[i]-sumc[j])+
s*(sumc[n]-sumc[j]} (0 <= j < i)
复杂度为O(n^2), 注意开LL
#include<bits/stdc++.h>
using namespace std;
const int N = 5010;
long long f[N], sumt[N], sumc[N];
int main(){
int n, s, t, c;
scanf("%d%d", &n, &s);
for(int i = 1;i <= n;i ++){
scanf("%d%d", &t, &c);
sumt[i] = sumt[i-1] + t;
sumc[i] = sumc[i-1] + c;
}
memset(f, 0x3f, sizeof f);
f[0] = 0;
for(int i = 1;i <= n;i ++){
for(int j = 0;j < i;j ++){
f[i] = min(f[i], f[j] + sumt[i]*(sumc[i]-sumc[j])+(long long)s*(sumc[n]-sumc[j]));
}
}
cout << f[n] << endl;
return 0;
}
任务安排2(AcWing 301)
题目唯一的改变即将n范围扩大至了3e5,显然O(n^2)的算法是无法通过的,引入斜率优化:
前面已经得到了式子:
f[i] = min{f[j]+sumt[i]*(sumc[i]-sumc[j])
+s*(sumc[n]-sumc[j])
即方程f[i] = f[j]-(sumt[i]+s)*sumc[j]
+sumt[i]*sumc[i]+s*sumc[n]
当我们把i视为常量时,式子中的sumt[i],s,sumc[i]
sumc[n],以及f[i]即为已知的值。
式子中仅剩变量f[j]和sumc[j].
不妨把变量分开写,出于习惯我们将没有系数项移至等式左边
得到:
f[j] = (sumt[i]+s)*sumc[j] +
f[i]-sumt[i]*sumc[i]-s*sumc[n]
观察等式很容易发现该式子为截距式 y = kx + b的形式
其中k = sumt[i] + s 为常数
b = f[i] - sumt[i]*sumc[i] - s*sumc[n]也为常数
即问题转化为给定一系列的点(sumc[j], f[j])
求使f[i]取得最小值的点
由于在 b 中f[i]为正,即b取最小值时,f[i]有最小值成立
但图中仍有n个点,均遍历一遍复杂度并没有得到改善
但观察后可以发现,在该情况下(k>0, b 中 f[i] 为正)
n个点中只有形成凸包的右下部分的点需要考虑
但在极端情况下可能所有点都符合条件,预期复杂度较差
我们再次观察k 和 b
由于 t [ i ] 大于0, 即sumt [ i ] 是严格单调递增的, 同时因为c [ i ] 大于0, 即sumc [ i ] 也是严格单调递增的。
即一个有趣的现象是
1 、斜率单调递增
2 、 新加入点的横坐标x单调递增
在拥有凸包基础构造知识的同志都能意识到,该过程即为凸包构造法中的Graham法,并且省去了预处理过程中对坐标的排序,由于x单调递增,所以保证了新加入的点一定在凸包的边上,并且根据队列中储存的凸包最后一个点和倒数第二个点的斜率k1 与新加入点与凸包边上最后一个/倒数第二个点的斜率k 比较, 若 k < k1 则说明此时的最后一个点在新形成的凸包的内部,删除队伍最后一个点,直到队列中的点满足性质为止。
0
由于斜率单调递增,即说明一旦凸包下边缘的第一条边斜率小于等于当前式子中的k值,即可删去该点,并保证在后面的查询中该点一定不是满足使f [i ] 最小的最优解。因此代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 300010;
typedef long long LL;
LL f[N], t[N], c[N], q[N];
int main(){
LL n, s;
scanf("%lld%lld", &n, &s);
for(int i = 1;i <= n;i ++){
scanf("%lld%lld", &t[i], &c[i]);
t[i] += t[i - 1];
c[i] += c[i - 1];
}
int tt = 0, hh = 0;
q[0] = 0;
for(int i = 1;i <= n;i ++){
while(hh < tt && (f[q[hh+1]]-f[q[hh]]) <= (__int128)(t[i]+s)*(c[q[hh+1]]-c[q[hh]])) hh ++;int j = q[hh];
f[i] = f[j] - (t[i] + s) * c[j] + t[i] * c[i] + s * c[n];
while (hh < tt && (__int128)(f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt - 1]]) >= (__int128)(f[i] - f[q[tt - 1]]) * (c[q[tt]] - c[q[tt - 1]])) tt -- ;
q[ ++ tt] = i;
}
cout << f[n] << endl;
return 0;
}
1
当斜率不一定单调递增时(即此题),就不能通过删除队首元素的方式优化查询,查询需要通过二分斜率实现,其余过程不变
#include<bits/stdc++.h>
using namespace std;
const int N = 300010;
typedef long long LL;
LL f[N], t[N], c[N];
int q[N];
int main(){
int n, s;
scanf("%d%d", &n, &s);
for(int i = 1;i <= n;i ++){
scanf("%lld%lld", &t[i], &c[i]);
t[i] += t[i - 1];
c[i] += c[i - 1];
}
int hh = 0, tt = 0;
q[0] = 0;
for(int i = 1;i <= n;i ++){
int l = hh, r = tt;
while(l < r){
int mid = l + r >> 1;
if((f[q[mid+1]]-f[q[mid]]) > (t[i] + s)*(c[q[mid+1]]-c[q[mid]])) r = mid;
else l = mid + 1;
}
int j = q[r];
f[i] = f[j] - (t[i] + s)*c[j] + t[i]*c[i] + (LL)s*c[n];
while(hh < tt && (__int128)(f[q[tt]]-f[q[tt-1]])*(c[i] - c[q[tt]]) >= (__int128)(f[i] - f[q[tt]])*(c[q[tt]]-c[q[tt-1]])) --tt;
q[++tt ] = i;
}
cout << f[n] << '\n';
return 0;
}
给出拓展题运输小猫
大概思路:
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010, M = 100010, P = 110;
LL f[P][M], d[N], a[M], t[M], s[M];
int q[M];
LL get_y(int k, int j){
return f[j-1][k] + s[k];
}
int main(){
int n, m, p;
scanf("%d%d%d", &n, &m, &p);
for(int i = 2; i <= n;i ++){
scanf("%lld", &d[i]);
d[i] += d[i - 1];
}
for(int i = 1;i <= m;i ++){
int h;
scanf("%d%lld", &h, &t[i]);
a[i] = t[i] - d[h];
}
sort(a+1, a+m+1);
for(int i = 1;i <= m;i ++) s[i] = s[i-1] + a[i];
memset(f, 0x3f, sizeof f);
for(int i = 0; i <= p;i ++) f[i][0] = 0;
for(int j = 1;j <= p;j ++){
int hh = 0, tt = 0;
q[0] = 0;
for(int i = 1;i <= m;i ++){
while(hh < tt && (get_y(q[hh + 1], j) - get_y(q[hh], j)) <= a[i] * (q[hh + 1] - q[hh])) hh ++;
int k = q[hh];
f[j][i] = f[j-1][k] + s[k] + a[i] * i - a[i] * k - s[i];
while(hh < tt && (get_y(q[tt], j) - get_y(q[tt - 1], j)) * (i - q[tt]) >= (get_y(i, j) - get_y(q[tt], j)) * (q[tt] - q[tt - 1])) tt --;
q[++ tt] = i;
}
}
printf("%lld\n", f[p][m]);
return 0;
}
小结:
斜率优化dp即在得出的状态转移方程中通过图形化分析,将方程转化为自变量和因变量的关系,维护一个更小范围的可行解,从而达到降低计算次数的目的,从原理方面我更愿意称其为凸包优化((