[算法01] 什么?斜率优化动态规划是啥?

肯定还有一些地方讲的不太对,留个坑以后再填

概述

全 文 转 移 方 程 中 的 状 态 都 为 f 。 全 文 两 点 间 的 斜 率 统 一 表 示 为 g [ x , y ] {\color{Red}全文转移方程中的状态都为 f。} \\{\color{Green}全文两点间的斜率统一表示为g[x,y]} fg[x,y]

众所周知, 计算斜率的式子大概长这样:
x i − x j y i − y j \frac{x_i-x_j}{y_i-y_j} yiyjxixj

用的上斜率优化的题目大概长啥样呢?它们的方程可以近似地看成这样:

f i = max ⁡ j = 1 i − 1 { f j + c i , j } f_i = \max_{j=1}^{i-1} \{f_j+c_{i,j}\} fi=j=1maxi1{fj+ci,j}

感性理解一下, 大概就是说每一个状态,都可以从之前的状态花费一定的代价转移过来。

一般这样的方程,每次计算,我们都需要遍历一遍之前的状态,这样会造成极大的时间浪费,达到 O ( n 2 ) O(n^2) O(n2) 甚至 O ( n 3 ) O(n^3) O(n3) 级别的时间复杂度。

如果你已经学习过了单调队列优化DP,那么你可能会想:

如果我计算 f i f_i fi 时,已经找到了之前状态的最佳转移点,那该多好?

其实斜率优化DP,就是通过一些技巧,令状态之间有比较优劣的依据,以此来用单调队列维护它们的单调性,从而使得每一次计算新的状态的时候,都可以直接从单调队列的队头取出最优的转移出发点来。

比如说状态 f i f_i fi 可以从 f j f_j fj f k f_k fk 转移而来,且满足 j < k < i j < k <i j<k<i

那么对于状态 f j f_j fj f k f_k fk 的优劣性,分如下几种情况讨论:

  • f j f_j fj 转移到 f i f_i fi 优于从 f k f_k fk 转移到 f i f_i fi, 但是很可惜,因为 k k k 的位置比 j j j 靠后,所以暂时先不能把 k k k 删掉。
  • f k f_k fk 转移到 f i f_i fi 优于从 f j f_j fj 转移到 f i f_i fi j j j 就可以 o u t out out 了。

这些其实就是单调队列的基本思想了。

接下来考虑一下怎么把斜率这鬼玩意给牵扯进来:

假设队列中现在有五个点,分别代表DP中的五个状态: a , b , c , d , e a,b,c,d,e a,b,c,d,e

首先保证,五个点中,任意两个点的转移式都可以表达成上文的斜率 x i − x j y i − y j \frac{x_i-x_j}{y_i-y_j} yiyjxixj 形式

因此,如果我们在图上将四对相邻的点的转移式画成一条斜率对应的直线——

在这里插入图片描述
我们会发现,此时, g [ a , b ] > g [ b , c ] < g [ c , d ] > g [ d , e ] g[a,b]>g[b,c]<g[c,d]>g[d,e] g[a,b]>g[b,c]<g[c,d]>g[d,e]

而用单调队列维护好的状态集合大概长这样:
在这里插入图片描述
此时, g [ a , b ] > g [ b , c ] > g [ c , d ] > g [ d , e ] g[a,b]>g[b,c]>g[c,d]>g[d,e] g[a,b]>g[b,c]>g[c,d]>g[d,e]

没错,我知道你会问,怎么保证用单调队列维护好斜率后,队首就是最佳的转移出发点呢?

下面是一两道简单的例题,用它们来讲讲吧。

例题1-特别行动队(BZOJ1911)

这一题算是比较简单的斜率优化了,式子也比较好推,没有那么反人类

这是洛谷的题面

本着人道主义精神,我还是决定把题面截图放在下面:
在这里插入图片描述接下来,我们通过这道题来了解一下,可以用斜率优化解决的题目大概长啥样。

设:

  • f i f_i fi 表示将前 i i i 名士兵划分成完整的若干组修正后最大战力值。
  • s i s_i si 表示前 i i i 名士兵的 x i x_i xi 之和。

一个很显然的方程就出来了,其实就是加上了 [ j + 1 , i ] [ j+1,i ] [j+1,i] 中的所有士兵划分为一队的花费,按照题意写下来就行。
f i = max ⁡ { f j + a ( s i − s j ) 2 + b ( s i − s j ) + c } f_i = \max\{f_j + a (s_i-s_j)^2+b (s_i - s_j) + c \} fi=max{fj+a(sisj)2+b(sisj)+c}
接下来比较从 f j f_j fj 转移到 f i f_i fi 和 从 f k f_k fk 转移到 f i f_i fi 的优劣性。

k < j < i k<j<i k<j<i

如果 选 j j j 优于选 k k k,那么显然满足下式:
f j + a ( s i − s j ) 2 + b ( s i − s j ) + c > f k + a ( s i − s k ) 2 + b ( s i − s k ) + c f_j + a (s_i-s_j)^2+b (s_i - s_j) + c > f_k + a (s_i - s_k)^2+b (s_i-s_k)+c fj+a(sisj)2+b(sisj)+c>fk+a(sisk)2+b(sisk)+c
首先显然可以将 c c c 项消去。接着运用完全平方公式拆开后,将 a s i 2 a {s_i}^2 asi2 b s i b {s_i} bsi 项消去。移项后可得:
f k − f j + a s k 2 − a s j 2 − b s k + b s j > 2 a s i s j − 2 a s i s k f_k-f_j+a{s_k}^2-a{s_j}^2-bs_k+bs_j > 2a{s_i}{s_j}-2a{s_i}{s_k} fkfj+ask2asj2bsk+bsj>2asisj2asisk
不等式右边的 s j − s k s_j-s_k sjsk 移到左边作为分母:
f k − f j + a s k 2 − a s j 2 − b s k + b s j s j − s k > 2 a s i \frac{f_k-f_j+a{s_k}^2-a{s_j}^2-bs_k+bs_j}{s_j-s_k} > 2a{s_i} sjskfkfj+ask2asj2bsk+bsj>2asi

将不等式左边整理一下:

( f k + a s k 2 − b s k ) − ( f j + a s j 2 − b s j ) s j − s k \frac{(f_k+a{s_k}^2-bs_k)-(f_j+a{s_j}^2-bs_j)}{s_j-s_k} sjsk(fk+ask2bsk)(fj+asj2bsj)

注意到了吗?这其实就是一个斜率式!!!

因此,这道题可以用斜率优化DP式。

计算斜率的代码至此就很显然了,下面的代码只算出了不等式的左边部分:

inline double work(ll k, ll j) {
    return (double) (f[k] - f[j] + a*s[k]*s[k] - a*s[j]*s[j] - b*s[k] + b*s[j]) / (s[k] - s[j]);
}

单调队列部分就更简单了:

  • 处理队首时,将计算出来的斜率和 2 a s i 2as_i 2asi 作比较即可。
  • 处理队尾时,注意维护此时状态集合的“下凸”性质即可(就是上面的第二张图)
for (int i = 1; i <= n; ++ i) {
    while (head + 1 <= tail && work(q[head], q[head + 1]) > 2 * a * s[i])
        ++ head;
		
    ll j = q[head];
    f[i] = f[j] + a * (s[i] - s[j]) * (s[i] - s[j]) + b * (s[i] - s[j]) + c;
	
    while (head + 1 <= tail && work(q[tail - 1], q[tail]) < work(q[tail], i))
        -- tail;
        
    q[++ tail] = i;
}

这里还有一个小小的细节需要注意:不能像往常那样写 head<=tail 判断队列是否为空,而要保证队列中至少还剩下 2 2 2 个元素

以下是完整代码:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
const int N = 1e6 + 10;
ll n, a, b, c, tmp, f[N], q[N], s[N];

inline double work(ll k, ll j) {
    return (double) (f[k] - f[j] + a * s[k] * s[k] - a * s[j] * s[j] - b * s[k] + b * s[j]) / (s[k] - s[j]);
}

int main() {
    scanf("%lld%lld%lld%lld", &n, &a, &b, &c);
    for (int i = 1; i <= n; ++ i) {
        scanf("%lld", &tmp);
        s[i] = s[i - 1] + tmp;
    }

    ll head = 1, tail = 1;
    for (int i = 1; i <= n; ++ i) {
        while (head + 1 <= tail && work(q[head], q[head + 1]) > 2 * a * s[i])
            ++ head;
		
        ll j = q[head];
        f[i] = f[j] + a * (s[i] - s[j]) * (s[i] - s[j]) + b * (s[i] - s[j]) + c;
		
        while (head + 1 <= tail && work(q[tail - 1], q[tail]) < work(q[tail], i))
            -- tail;
		
        q[++ tail] = i;
    }

    printf("%lld", f[n]);
    return 0;
}

小结

因此,斜率优化的DP式子需要能够化成斜率式的形式,即为:
x i − x j y i − y j \frac{x_i-x_j}{y_i-y_j} yiyjxixj

其中, x i , y i x_i,y_i xi,yi项与下标值包含 a a a 的项有关,也就是不能有下标包含 b b b x j , y j x_j,y_j xj,yj 反之亦然。

拿上面那道题的不等式左边来理解一下:
( f k + a s k 2 − b s k ) − ( f j + a s j 2 − b s j ) s j − s k \frac{(f_k+a{s_k}^2-bs_k)-(f_j+a{s_j}^2-bs_j)}{s_j-s_k} sjsk(fk+ask2bsk)(fj+asj2bsj)

可以将其变成:
( − f j − a s j 2 + b s j ) − ( − f k − a s k 2 + b s k ) s j − s k \frac{(-f_j-a{s_j}^2+bs_j)-(-f_k-a{s_k}^2+bs_k)}{s_j-s_k} sjsk(fjasj2+bsj)(fkask2+bsk)

可以发现,这样就满足上述性质了。

因此,我们对于每一道斜率优化DP题目的目标就是将DP式子转化成:

x i , y i x_i,y_i xi,yi项与下标值包含 a a a 的项有关,也就是不能有下标包含 b b b x j , y j x_j,y_j xj,yj 反之亦然。

再讲两道例题,加深一下印象吧。

例题2-打印文章(HDU3507)

例题3-仓库建设(BZOJ1096)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值