斜率优化dp小结(convex hull trick)

本文介绍了如何运用斜率优化动态规划解决任务调度问题,通过将问题转化为寻找最优凸包,从而降低时间复杂度。在AcWing300和301两道题目中,分别展示了在任务数量较小和较大时,如何应用斜率优化策略来减少计算量,以实现高效求解。此外,还给出了一个拓展问题——运输小猫,进一步阐述了斜率优化在实际问题中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一个悲惨的前言
斜率优化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即在得出的状态转移方程中通过图形化分析,将方程转化为自变量和因变量的关系,维护一个更小范围的可行解,从而达到降低计算次数的目的,从原理方面我更愿意称其为凸包优化((

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

marx97 ٩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值