动态规划——斜率优化DP

题目清单

acwing300.任务安排1

状态表示fi:

集合:完成前i个任务且第i个任务为最后一个批次最后一个任务的方案。

属性:min
状态计算:
f i = m i n { f j + s u m t i × ∑ j + 1 i w u + s × ∑ j + 1 n w i } fi=min\{fj+sumti ×\sum_{j+1}^{i}wu+s×\sum_{j+1}^{n}wi\} fi=min{fj+sumti×∑j+1iwu+s×∑j+1nwi} ( 0 ≤ j < i ) (0\leq j < i) (0≤j<i)
f i = m i n { f j + s u m t i × ( s u m c i − s u m c j ) + s × ( s u m c n − s u m c j ) } fi=min\{fj+sumti ×(sumci-sumcj)+s×(sumcn-sumcj)\} fi=min{fj+sumti×(sumci−sumcj)+s×(sumcn−sumcj)} ( 0 ≤ j < i ) (0\leq j < i) (0≤j<i)

时间复杂度为 O ( n 2 ) O(n^2) O(n2)

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 5010;
typedef long long ll;
ll f[N];
ll sumt[N], sumc[N];
int n, s;
int main()
{
    cin >> n >> s;
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%lld%lld", &sumt[i], &sumc[i]);
        sumt[i] += sumt[i - 1];
        sumc[i] += sumc[i - 1];
    }
    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]) + s * (sumc[n] - sumc[j]));
        }
    }
    cout << f[n];
    return 0;
}

acwing301.任务安排2

我们对上一题得到的状态转移方程 f i = m i n { f j + s u m t i × ( s u m c i − s u m c j ) + s × ( s u m c n − s u m c j ) } fi=min\{fj+sumti ×(sumci-sumcj)+s×(sumcn-sumcj)\} fi=min{fj+sumti×(sumci−sumcj)+s×(sumcn−sumcj)} ( 0 ≤ j < i ) (0\leq j < i) (0≤j<i)进行编变形。

f i = f j + s u m t i × s u m c i − s u m t i × s u m c j + s × s u m c n − s × s u m c j fi=fj+sumti ×sumci-sumti×sumcj+s×sumcn-s×sumcj fi=fj+sumti×sumci−sumti×sumcj+s×sumcn−s×sumcj
f i = f j − s u m t i × s u m c j − s × s u m c j + s u m t i × s u m c i + s × s u m c n fi=fj-sumti×sumcj-s×sumcj+sumti ×sumci+s×sumcn fi=fj−sumti×sumcj−s×sumcj+sumti×sumci+s×sumcn
f i = f j − ( s u m t i + s ) × s u m c j + s u m t i × s u m c i + s × s u m c n fi=fj-(sumti+s)×sumcj+sumti ×sumci+s×sumcn fi=fj−(sumti+s)×sumcj+sumti×sumci+s×sumcn

移项得到
f j = ( s u m t i + s ) × s u m c j − s u m t i × s u m c i − s × s u m c n + f i fj=(sumti+s)×sumcj-sumti ×sumci-s×sumcn+fi fj=(sumti+s)×sumcj−sumti×sumci−s×sumcn+fi

上式是 y = k x + b y=kx+b y=kx+b的形式, y y y是 f j fj fj, k k k是 ( s u m t i + s ) (sumti+s) (sumti+s), x x x是 s u m c j sumcj sumcj, b b b是 f i − s u m t i × s u m c i − s × s u m c n fi-sumti ×sumci-s×sumcn fi−sumti×sumci−s×sumcn

我们的目标是求 f i fi fi的最小值,因为 b b b中除了 f i fi fi以外的部分是常量,也正因此等价于去想办法让截距最小。因为 k k k也是一个常量, 所以本问题又可以转化为给定一条斜率固定的直线去找过一个 ( x , y ) (x,y) (x,y)点对,使得 b b b最小。 ( x , y ) (x,y) (x,y)点对有 ( s u m c 0 , f 0 ) (sumc0,f0) (sumc0,f0), ( s u m c 1 , f 1 ) (sumc1,f1) (sumc1,f1),..., ( s u m c i − 1 , f i − 1 ) (sumci-1,fi-1) (sumci−1,fi−1)。

给出结论,无论斜率如何变化,最小值一定是取到凸包下边界的一个点。

具体来说,最小值一定是第一个大于直线斜率的线段头部 k i k_i ki的点。

分析题目:

1., k k k是 ( s u m t i + s ) (sumti+s) (sumti+s),单调递增。

2.新加入点的横坐标也是单调递增。

由于斜率 k k k是单调递增的,也就是说,如果当前的斜率 k k k大于队头两个点的斜率时,那么一开始的点就可以彻底删除,在以后也不会用到,所以凸包中的点可以用单调队列来维护。

维护一个凸包的做法:

1.查询的时候,把队头小于当前斜率k的点删掉。
f q \[ h h + 1 − f q \[ h h s u m c q \[ h h + 1 ] − s u m c q \[ h h ≤ s u m t i + s \frac{fq\[hh+1-fq\[hh}{sumcq\[hh+1]-sumcq\[hh}\leq sumti+s sumcq\[hh+1]−sumcq\[hhfq\[hh+1−fq\[hh≤sumti+s

2.插入的时候,把不在凸包上的点删掉。
f q \[ t t − f q \[ t t − 1 s u m c q \[ h h ] − s u m c q \[ h h − 1 ≥ f i − f q \[ t t ] s u m c i − s u m c q \[ h h ] \frac{fq\[tt-fq\[tt-1}{sumcq\[hh]-sumcq\[hh-1}\geq \frac{fi-fq\[tt]}{sumci-sumcq\[hh]} sumcq\[hh]−sumcq\[hh−1fq\[tt−fq\[tt−1≥sumci−sumcq\[hh]fi−fq\[tt]

时间复杂度为 O ( n ) O(n) O(n)

cpp 复制代码
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 300010;
int q[N];
ll t[N], c[N];
ll f[N];
int n, s;
int main()
{
    cin >> 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;
    for (int i = 1; i <= n; i ++ )
    {
        while (hh < tt && f[q[hh + 1]] - f[q[hh]] <= (t[i] + s) * (c[q[hh + 1]] - c[q[hh]])) hh ++ ;
        int j = q[hh];
        f[i] = f[j] + t[i] * (c[i] - c[j]) + s * (c[n] - c[j]);
        while (hh < tt && (f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt]]) >= (f[i] - f[q[tt]]) * (c[q[tt]] - c[q[tt - 1]])) tt -- ;
        q[++ tt] = i;
    }
    cout << f[n];
    return 0;
}

acwing302.任务安排3

和上一题不同的是, 本题中机器的工作时间可以为负数,也正因此斜率 k k k并不是随着 i i i单调递增的了,上一题中,对于凸包的头部和尾部均使用单调队列来处理,本题则需要使用二分去找到合适的点对,对尾部的处理同样使用单调队列。

cpp 复制代码
#include <iostream>
using namespace std;
const int N = 300010;
typedef long long ll;
ll t[N], c[N];
ll f[N];
int q[N];
int n, s;
int main()
{
    cin >> 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;
    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]] > (double)(t[i] + s) * (c[q[mid + 1]] - c[q[mid]])) r = mid;
            else l = mid + 1;
        }
        int j = q[l];
        f[i] = f[j] - (t[i] + s) * c[j] + t[i] * c[i] + s * c[n];
        while (hh < tt && (double)(f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt]]) >= (double)(f[i] - f[q[tt]]) * (c[q[tt]] - c[q[tt - 1]])) tt -- ;
        q[++ tt] = i;
    }
    cout << f[n];
    return 0;
}

acwing303.运输小猫

每一只小猫都会对应一个最早出发时间 a i = t i − d h \[ i ] ai=ti-dh\[i] ai=ti−dh\[i],也就是结束玩耍的时间减去到1号山的距离。当我们处理完 a a a数组后,对数组 a a a进行一个从小打到的排序。这样一来,相邻一段猫肯定是要由一个饲养员接走的,饲养员的出发时间就是这一段最后一只猫对应的数组 a a a的值。
状态表示fij:

集合:对于前 i i i只猫,安排 j j j个饲养员去接猫的所有方案。

属性:min
状态计算:
f i j = m i n { f k j − 1 + ∑ k + 1 i ( a i − a u ) } fij=min\{fkj-1+\sum_{k+1}^{i}(a_i-a_u)\} fij=min{fkj−1+∑k+1i(ai−au)} ( 0 ≤ k < i ) \ \ \ (0\leq k <i) (0≤k<i)
f i j = m i n { f k j − 1 + ∑ k + 1 i a i − ∑ k + 1 i a u } fij=min\{fkj-1+\sum_{k+1}^{i}a_i-\sum_{k+1}^{i}a_u\} fij=min{fkj−1+∑k+1iai−∑k+1iau} ( 0 ≤ k < i ) \ \ \ (0\leq k <i) (0≤k<i)
f i j = m i n { f k j − 1 + ( i − k ) a i − ( s i − s k ) } fij=min\{fkj-1+(i-k)a_i-(si-sk)\} fij=min{fkj−1+(i−k)ai−(si−sk)} ( 0 ≤ k < i ) \ \ \ (0\leq k <i) (0≤k<i)

移项得:
f k j − 1 + s k = a i × k + f i j + s i − i a i fkj-1+sk=ai ×k+fij+si-iai fkj−1+sk=ai×k+fij+si−iai

至此,本题就转换为了任务安排2。

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
const int N = 100010;
const int P = 110;
ll f[P][N];
ll a[N];
ll s[N];
int d[N];
int q[N];
int n, m, p;
ll get_y(int j, int k)
{
    return f[j - 1][k] + s[k];
}
int main()
{
    cin >> n >> m >> p;
    for (int i = 2; i <= n; i ++ )
    {
        scanf("%d", &d[i]);
        d[i] += d[i - 1];
    }
    for (int i = 1; i <= m; i ++ )
    {
        ll h, t;
        scanf("%lld%lld", &h, &t);
        a[i] = t - d[h];
    }
    sort(a + 1, a + m + 1);
    for (int i = 1; i <= m; i ++ )
    {
        s[i] = a[i];
        s[i] += s[i - 1];
    }
    memset(f,0x3f,sizeof f);
    for(int i = 0;i <= p;i ++)
        f[i][0] = 0;

    for (int i = 1; i <= p; i ++ )
    {
        int hh = 0, tt = 0;
        for (int j = 1; j <= m; j ++ )
        {
            while (hh < tt && get_y(i, q[hh + 1]) - get_y(i, q[hh]) <= (q[hh + 1] - q[hh]) * a[j]) hh ++;
            int k = q[hh];
            f[i][j] = f[i - 1][k] + (j - k) * a[j] - (s[j] - s[k]);
            while (hh < tt && (get_y(i, q[tt]) - get_y(i, q[tt - 1])) * (j - q[tt]) >= (get_y(i, j) - get_y(i, q[tt])) * (q[tt] - q[tt - 1])) tt --;
            q[++ tt] = j;
            
        }
    }
    cout << f[p][m];
    return 0;
}
相关推荐
sali-tec15 分钟前
C# 基于OpenCv的视觉工作流-章81-弯脚检测
图像处理·人工智能·opencv·算法·计算机视觉
kkeeper~16 分钟前
0基础C语言积跬步之自定义类型联合和枚举
c语言·开发语言·算法
昵称好难啊17 分钟前
4.OpenClaw源码解析_路由的概念
人工智能·算法
Bingorl22 分钟前
机器学习之KNN算法
人工智能·算法·机器学习
muddjsv23 分钟前
《算法导论》入门学习路径:从零基础到系统掌握
学习·算法
KaMeidebaby41 分钟前
卡梅德生物技术快报|组蛋白乙酰化修饰调控动脉粥样硬化的分子机制及中药表观干预研究
网络·人工智能·网络协议·tcp/ip·算法
Fms_Sa41 分钟前
分治法—最大子段问题
算法·c#
Galerkin码农选手1 小时前
awq_marlin和gptq_marlin量化算法简要介绍
算法
buhuizhiyuci1 小时前
【算法篇】动态规划——斐波那契数列模型
算法·动态规划
棱镜研途1 小时前
学习笔记丨模式识别与机器学习5大核心赛道解析(IC-IPPR 2026)
人工智能·神经网络·算法·机器学习·模式识别·学术会议·智能计算