浅谈斜率优化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)}的函数图像

image
就说这个是决策点的图像,那么我们的凸包长什么样呢
image
紫色的线就是我们要维护的东西,也就是所有函数取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:这个具体的参数需要自己去算,不能套用!但基本上格式都长一个样子

蒟蒻太菜了,如有错误请指出,我尽快修正!(虽然根本没时间就是了)
posted @ 2026-08-25 00:47  DK_orz  阅读(3)  评论(0)    收藏  举报