浅谈斜率优化dp
首先什么是斜率优化dp?
直接讲太抽象了,我们来看一道例题:
有 N 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。机器会把这 N 个任务分成若干批,每一批包含连续的若干个任务。从时刻 0 开始,任务被分批加工,执行第i个任务所需的时间是 Ti。另外,在每批任务开始前,机器需要 S 的启动时间,故执行一批任务所需的时间是启动时间S加上每个任务所需时间之和。
一个任务执行后,将在机器中稍作等待,直至该批任务全部执行完毕。也就是说,同一批任务将在同一时刻完成。每个任务的费用是它的完成时刻乘以一个费用系数 Ci 。
请为机器规划一个分组方案,使得总费用最小。
对于全部数据,1≤N≤104,0≤S≤50,1≤Ti,Ci≤100
我们设dp[i]是安排前i个任务的最小费用,用前缀和优化一下不难得到下面这个dp式子,(读者可以暂时不管这个式子怎么得到的,因为这不是本文重点)
dp[i]= (0<=j<i)min{dp[j]+(sumT[i]−sumT[j]+S)×(sumC[N]−sumC[j])}
好那么我们现在有了一个含min的dp转移式,接下来就是要把这个式子的括号展开:
dp[i] = (0<=j<i)min{dp[j] + (sumT[i]⋅sumC[N])−(sumT[i]⋅sumC[j])−(sumT[j]⋅sumC[N])+(sumT[j]⋅sumC[j])+(S⋅sumC[N]−S⋅sumC[j])}
看着好吓人,不过你这个在草稿纸上就能轻松完成,接下来就是把min里的这些式子分类,只包含j的分一类,只包含i或常数的分一类,既有j又有i的分一类
dp[i] = (0<=j<i)min{(-sumT[i]⋅sumC[j])+(dp[j]−(sumT[j]⋅sumC[N])+(sumT[j]⋅sumC[j])+(−S⋅sumC[j]))}+(sumT[i]⋅sumC[N])+(S⋅sumC[N])
有i有j 只有j 只有i和常数
注意到我把i和常数分到了min外面,为什么?这里就要扯到一个关于决策点的概念,我们枚举到了i,那么对于i来说,有关i的值(像sumC[i])可以看做是常数,我们其实是要选一个点j,j就是决策点,是真正影响我们dp的因素
我们想,如果min里面全是关于j的,那么我们是不是只需要用单调队列dp就行,但问题是里面i会影响min的结果,而我们注意到里面只有一项与i有关的项,那么min里面就可以写成一个斜线的形式 :y = kx+b
我们说k = -sumT[i]
x = sumC[i]
b = 只有j的部分
那么min里的不就只有个kx+b吗?所以dp[i] = (0<=j<i)min{y = (kx+b)} + 只有i和常数
为什么要这么做呢?为的是把决策点转换一条斜线,当i取一个值时返回min里面的值,相当于一个函数关系
最后我们说要维护一个凸包,这个凸包是什么呢?就是这个min{y = (kx+b)}的函数图像

就说这个是决策点的图像,那么我们的凸包长什么样呢

紫色的线就是我们要维护的东西,也就是所有函数取min的函数
得到这个之后就好办了,直接kx+b就行。
具体怎么维护呢?用一个单调队列,因为这题的斜率是单调的,x也是单调的,所以当队首发现旧的要劣于新的时,那么它就永远也不可能再取到了,(因为他的增长速度跟慢,起步更少,自然就比不过新的)
然后新增一条直线时就是看他和倒数第一和倒数第二条直线比较,具体的可以看图2的蓝线,如果说我们已经有绿线和蓝线,新增的是红线,他紫线里是没有蓝线的!也就是说蓝线永远也选不到!那我们怎么判定呢?红线和蓝线的x坐标<=红线和绿线的y坐标
这个东西每一个题有每一个题做法,你只要去画图,然后取一个特例观察就行,总体上还是很公式化的
```cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 10005;
long long sumT[N], sumC[N], dp[N];
int n, S;
struct Line {
long long k, b;
long long solve(long long x) {
return k * x + b;
}
};
deque<Line> dq;
Line getLine(int j) {
Line l;
l.k = -sumC[j];
l.b = dp[j] - sumT[j] * sumC[n] + sumT[j] * sumC[j] - S * sumC[j];
return l;
}
// 判断 l2 是否被 l1 和 l3 完全覆盖(上凸包)
bool bad(Line l1, Line l2, Line l3) {
// (b2 - b1) * (k2 - k3) >= (b3 - b2) * (k1 - k2)
return (l2.b - l1.b) * (l2.k - l3.k) >= (l3.b - l2.b) * (l1.k - l2.k);
}
int main() {
cin >> n >> S;
for (int i = 1; i <= n; i++) {
cin >> sumT[i] >> sumC[i];
sumT[i] += sumT[i-1];
sumC[i] += sumC[i-1];
}
dq.push_back(getLine(0)); // j=0
for (int i = 1; i <= n; i++) {
long long x = sumT[i];
// 弹出队首:如果队首不如第二个优
while (dq.size() >= 2 && dq[0].solve(x) >= dq[1].solve(x))
dq.pop_front();
dp[i] = dq.front().solve(x) + sumT[i] * sumC[n] + S * sumC[n];
Line newLine = getLine(i);
// 弹出队尾:维护凸包
while (dq.size() >= 2 && bad(dq[dq.size()-2], dq[dq.size()-1], newLine))
dq.pop_back();
dq.push_back(newLine);
}
cout << dp[n] << endl;
return 0;
}
最后总结一下:
斜率优化是很公式化的,第一步:得到转移方程,第二步:把他进行数学转换,并把项分组,第三步:用单调队列维护凸包,(或二分或李超线段树)
使用条件,带min或max,里面只有一项带i,(可以写成kx+b的形式)
PS:如果你不理解或者有bug调不出来,可以打印中间变量,然后放到Demos里:)[https://www.desmos.com/]()
PSS:这个具体的参数需要自己去算,不能套用!但基本上格式都长一个样子
蒟蒻太菜了,如有错误请指出,我尽快修正!(虽然根本没时间就是了)

浙公网安备 33010602011771号