学习笔记:斜率优化 DP

斜率优化

引入

首先给出一种更加简单的优化。考虑一个这样的的式子:fi=min0j<i{fj+gj+hi} 不难看出,这个式子中的每一项只会与 ij 中的一个有关。显然可以转化为:f(i)=min0j<i{fj+gj}+hi 具体地,,我们可以考虑在转移的同时维护一个值 minval=fj+gj。对于每一次转移,我们先令 fi=minv+hi,再令 minv=min(minv,fj+gj),这样就实现了 O(n) 的转移。

然而,对于以下这种含有同时与 ij 有关的项的式子,这种优化就显得有些力不从心了:f(i)=min0j<i{fj+gi+hi+j} 在这种情况下,我们就需要考虑更优的做法了。

实现

首先搬一道例题。

P3628 [APIO2010] 特别行动队

形式化题意:给定一个长度为 n 的序列 x,将这个序列分成若干块。

具体地,每一块的权值为 aX2+bX+c,其中 abc 是给定的系数。

对于 X,我们定义当前块的区间为 [l,r],则 X=i=lrxi

试找出一种方案使得 X 最大,并求出这个最大值。

我们可以很快地写出一个状态转移方程:fi=min0j<i{fj+a×(gigj)2+b×(gigj)+c}gi=gi1+xi 然而这个式子暴力转移的时间复杂度为 O(n2),稳稳 TLE。

考虑斜率优化。首先浅浅推导一下:fi=min0j<i{fj+a×(gigj)2+b×(gigj)+c}fi=min0j<i{fj+a×gi22×a×gi×gj+a×gj2+b×gib×gj+c} 显然某些项只与 i 有关,我们将它们都提出来:fi+a×gi2+b×gi+c=min0j<i{fj2×a×gi×gj+a×gj2b×gj} 对于一些只与 j 有关的项,显然无论如何这些项都不会随 i 的变化而变化

我们令 hj=fj+a×gj2b×gj

则原式化为 fi+a×gi2+b×gi=min0j<i{hj2×a×gi×gj}

再令 si=2×a×gi

则原式化为 fi+a×gi2+b×gi+c=min0j<i{hj+si×gj}

再分别令 y=fi+a×gi2+b×gi+cx=gjk=sib=hj。则原式化为:y=kx+b 显然这就是直线的斜截式方程

考虑如何将 k 值(即 si)最大化。

如果 j1<j2j1 不比 j2 优,当且仅当 hj1+si×gj1hj2+si×gj2

移项得 si×(gj1gj2)hj2hj1

由于序列中的每个数都是非负数,所以 gj2>gj1,即 gj1gj2<0

不等式左右两边同时除以 gj1gj2 得:sihj2hj1gj1gj2khj2hj1gj1gj2si=2×a×gi 得:2×a×gihj2hj1gj2gj1 不等式左右两边同时乘 1 得:hj2hj1gj1gj22×a×gi 可以发现,不等式右边是一个斜率的表达式。更具体地,假设在平面直角坐标系上有两个点 A(gj1,hj1)B(gj2,hj2),那么 hj2hj1gj2gj1 此时就等价于过 AB 两点的直线的斜率。此时可以采用斜率优化

首先提炼一下之前推式子得到的结论:如果两个点相连所成的直线的斜率不大于 2×gi,那么前面的点所对应的 j 就必然不是最优决策点;反之,后面的点所对应的 j 就必然不是最优决策点。

先来考虑一下 3 个点的简化情况:如下图所示,显然有 k12>k23

图中的每一个点都对应着一个 j

可以发现,无论如何 2 都不会成为最优决策点。

证明:

  1. k23>k 时,2 才有可能成为最优决策点,此时有 l23>k
  2. k12>k 时,2 才有可能成为最优决策点,此时有 l12k

l12>l23,所以不存在任意一个实数 k 使得 l23>kl12k

综上所述,2 无论如何都不能成为最优决策点。证毕

所以这个点已经寄了,我们可以直接将它删去。

我们已经完美解决了 3 个点的情况,现在让我们试着将结论推广到题目的一般情况。

可以发现,如果存在三个横坐标递增的点,满足前两个点的斜率大于等于后两个点的斜率,那么就可以删去中间的那个点

所以,如果我们处理出一个不可删点的点集的斜率数组(每相邻两个数的斜率),那么这个数组必然是递增的。

不难发现,在一个不可删点的点集中,最优决策点 j 满足:

  1. j1 与点 j 相连所成直线的斜率不大于 k
  2. j 与点 j+1 相连所成直线的斜率大于 k

不难看出,这些点所成直线的斜率具有单调性,考虑通过二分答案来寻找最优决策点。

具体地,我们枚举 i,维护不可删点点集 A。设 A 的长度为 len,两个点 ij 相连所成直线的斜率为 kij

  1. 二分答案找到最优决策点 j
  2. 执行状态转移。
  3. 不停弹出队尾直到:kAlen1AlenkAleniA1,这里的 i 指的是 i 的对应点。
  4. i 加入 A 中。

总的时间复杂度为 O(nlogn)

斜率优化的精髓在于不可删点点集的单调性。正是因为这个单调性,我们才能采用二分去快速寻找最优决策点,从而将时间复杂度优化为 O(nlogn)

对于一些特定的题目,我们可以去掉 log。如对于本题(特别行动队)而言,k 是有单调性的;所以,每次我们可以不停地删去队头,直到队头满足最优决策点的性质;删完之后队头就是最优决策点。此时,每个元素至多入队一次出队一次,时间复杂度 O(n)

现在来看看这道题的代码。

斜率优化的代码历来非常短,甚至连1KB都没有,但是细节非常多。斜率优化的注意事项非常重要,这里有必要阐述一下:

  1. 斜率优化可能爆精度,比较两个斜率的时候可以交叉相乘。
  2. 如果不可删点点集的大小是 1 就不用再删点了。
  3. 在开始转移之前不可删点点集中有且仅有一个数 0
  4. 输出不要用 double,不然会自动转化为科学计数法的形式。
  5. 计算斜率必须 long double,除非采用交叉相乘法。
  6. 只有 k 具有单调性时才能用队列,否则只能 O(nlogn) 二分答案。

通常来说 O(n2) 的暴力转移都不难,可以考虑用对拍来查错(但是笔者现在忘得差不多了,这里提一嘴,先鸽着)。

#include <iostream>
#define int long long
#define double long double
#define MAXN 1000005
using namespace std;
int n, a, b, c;
int x[MAXN], f[MAXN], g[MAXN];
int q[MAXN], head = 1, tail = 1;
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
double getx(int x){return g[x];}
double gety(int x){return a * g[x] * g[x] - b * g[x] + f[x];}
double getk(int x, int y){return (gety(y) - gety(x))/(getx(y) - getx(x));}
signed main(){
    n = read();a = read();b = read();c = read();
    for(int i = 1 ; i <= n ; i ++)x[i] = read();
    for(int i = 1 ; i <= n ; i ++)g[i] = g[i - 1] + x[i];
    for(int i = 1 ; i <= n ; i ++){
        while(head < tail && (a << 1) * g[i] <= getk(q[head], q[head + 1]))head++;
        int j = q[head];
        f[i] = f[j] + a * (g[i] - g[j]) * (g[i] - g[j]) + b * (g[i] - g[j]) + c;
        while(head < tail && getk(q[tail - 1], q[tail]) <= getk(q[tail], i))tail--;
        q[++tail] = i;
    }
    cout << f[n] << endl;return 0;
}

一些练习

Luogu P5785,Luogu P3195

这里顺便给出 Luogu P3195 的代码:

#include <iostream>
#define int long long
#define double long double
#define MAXN 50005
using namespace std;
int n, l, c[MAXN];
double f[MAXN], g[MAXN];
int q[MAXN], head = 1, tail = 1;
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
double geta(int x){return g[x] + x;}
double getb(int x){return geta(x) + l + 1;} 
double getx(int x){return getb(x);}
double gety(int x){return f[x] + getb(x) * getb(x);}
double getk(int x, int y){return (gety(x) - gety(y))/(getx(x) - getx(y));}
signed main(){
    n = read();l = read();
    for(int i = 1 ; i <= n ; i ++)c[i] = read();
    for(int i = 1 ; i <= n ; i ++)g[i] = g[i - 1] + c[i];
    for(int i = 1 ; i <= n ; i ++){
        while(head < tail && geta(i) * 2 > getk(q[head], q[head + 1]))head++;
        f[i] = f[q[head]] + (geta(i) - getb(q[head])) * (geta(i) - getb(q[head]));
        while(head < tail && getk(i, q[tail - 1]) < getk(q[tail - 1], q[tail]))tail--;
        q[++tail] = i;
    }
    cout << (long long)f[n] << endl;return 0;
}
posted @   tsqtsqtsq  阅读(9)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示