斜率优化DP学习笔记

我前段时间在运用模拟退火算法骗分时,发现许多我用模拟退火通过的题正解都是斜率优化DP。感觉骗分不道德,于是我就给自己挖了一个坑。

算是学了点斜率优化DP,于是写点东西记录一下学习历程。

首先,斜率优化DP是基于决策单调性,于是我回去复习了一下单调队列,顺便做了几道单调队列和贪心。

1.单调队列

我经常被单调队列,于是复习起来也算是顺利。

例题就是让我们求一个序列长度不超过 mm 的最大子段和。可以利用单调队列的单调性排除不优秀的决策,从而大大提升算法效率。

我们先判断当前对头的长度是否与当前坐标超出极限长度 mm ,如果超出则出队。

while(q.size()&&i-q.front()>m)q.pop_front();

同时更新答案,注意一下 ss 数组是原序列的前缀和数组

ans=max(ans,s[i]-s[q.front()]); 

倒数第二步是单调队列的精髓,从队尾出队。

当一个数处在的位置比另一个数远离当前位置,但是前缀和又较大。

这是这个位置对于另一个位置来说是一个无用的位置,要及时出队。

换句话来理解,假如有一位初二的大佬进了省队,而我被初赛为难了两年,这是我对于这位大佬来讲就是一个完全没用的(我高一),要及时把我清除。

于是各位可以根据我的故事模拟出单调队列的出队过程。

while(q.size()&&s[q.back()]>=s[i])q.pop_back();

因为当前进行到 ii 点,而队列里的位置全部都在 ii 点的前面,于是这时候有位置的前缀和比 ii 点大,那么这个位置相对于 ii 对答案毫无贡献,于是出队。

算法进行下来,每个位置至多入队一次,出队一次。于是算法的时间复杂度是 O(N)O(N) 。

2.单调队列优化DP

例题是一道经典的单调队列优化DP

memset(dp,-0x3f,sizeof(dp));
dp[1]=a[1];
for(int i=1;i<=n;i++){
    for(int j=i-r;j<=i-l;j++)if(j>=1)dp[i]=max(dp[i],dp[j]+a[i]);
    if(i+r>n)ans=max(ans,dp[i]);
}

这是暴力DP的核心代码,时间复杂度完全随机数据是 O(n\log n)O(nlogn), 但是有规律的数据会被卡到 O(n^2)O(n2) ,于是我们要改进算法。

首先我们可以用树状数组优化,维护区间最小值,在这里不展开叙述。

单调队列的优化是复杂度最低的,是 O(n)O(n) 的时间复杂度。

memset(dp,-0x3f,sizeof(dp));
dp[0]=0;
for(int i=l;i<=n;i++){
    while(q.size()&&dp[q.back()]<=dp[i-l])q.pop_back();
    //当前决策优于队尾,从队尾弹出最优决策。
    q.push_back(i-l);
    while(q.front()+r<i&&q.size())q.pop_front();
    //如果长度过大,弹出队头。
    dp[i]=dp[q.front()]+a[i];
    if(i+r>n)ans=max(ans,dp[i]);
}

实际上单调队列可以很随意,队头队尾换顺序无所谓,因为单调队列理论上是基于双端队列。

3.斜率优化

斜率优化DP,实际上是一种特殊的单调队列优化DP,是利用线性规划排除不优秀的决策。

#include<cstdio>
#include<algorithm>
#include<cstring>
#define N 11451
#define int long long
using namespace std;
int n,s,st[N],sc[N],dp[N];
signed main(){
    scanf("%lld%lld",&n,&s);
    for(int i=1;i<=n;i++)scanf("%lld%lld",&st[i],&sc[i]),st[i]+=st[i-1],sc[i]+=sc[i-1];
    memset(dp,0x3f,sizeof(dp));
    dp[0]=0;
    for(int i=1;i<=n;i++){
        for(int j=0;j<i;j++){
            dp[i]=min(dp[i],dp[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]));
        }
    }
    printf("%lld",dp[n]);
    return 0;
}

例题的数据范围很小,可以用上面的 O(n^2)O(n2) 暴力DP通过,实际上有更劣的 O(n^3)O(n3) 的做法,这里不展开说明。

我们观察状态转移方程:

dp[i]=min(dp[i],dp[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]));

把 min 拿掉,得到一个柿子:

dp_i=dp_j+st_i\times sc_i+s\times sc_n-(s+st_i)\times sc_jdpi=dpj+sti×sci+s×scn(s+sti)×scj

dp_j=(s+st[i])\times sc_j-(dp_i+s\times sc_n-st_i\times sc_i)dpj=(s+st[i])×scj(dpi+s×scnsti×sci)

珂以转化一次函数标准柿子:

y=dp_j,x=sc_j,k=(s+st[i]),b=-(dp_i+s\times sc_n-st_i\times sc_i)y=dpj,x=scj,k=(s+st[i]),b=(dpi+s×scnsti×sci)

当 x,yx,y 为定值时,斜率越大截距越小。

于是我们用单调队列维护,要答案尽可能小,当斜率小于当前节点出队。

同时更新答案,再在队尾判断斜率,出队。

l=r=1;memset(dp,0x3f,sizeof(dp));dp[0]=0,q[1]=0;
for(int i=1;i<=n;i++){
    while(l<r&&dp[q[l+1]]-dp[q[l]]<=(s+st[i])*(sc[q[l+1]]-sc[q[l]]))l++;
    dp[i]=dp[q[l]]+st[i]*sc[i]+s*sc[n]-(s+st[i])*sc[q[l]];
    while(l<r&&(dp[q[r]]-dp[q[r-1]])*(sc[i]-sc[q[r]])>=(sc[q[r]]-sc[q[r-1]])*(dp[i]-dp[q[r]]))r--;
    q[++r]=i;
}

这里精度不过,斜率的比例柿通过移项改成了以上形式。

4.不再具有单调性?

任务安排中,当决策的斜率之间不在具有单调性,这是我们要如何高效处理决策?

例题就是如此,时间消耗为负数,所以斜率之间的关系不在具有单调性。

于是我们通过二分查找出斜率小于当前最优决策:

int p=0;
if(l==r)p=q[l];
else{
    int fi=l,lz=r,k=s+st[i];
    while(fi<lz){
        int mid=(fi+lz)/2;
        if(dp[q[mid+1]]-dp[q[mid]]<=k*(sc[q[mid+1]]-sc[q[mid]]))fi=mid+1;
        else lz=mid;
    }
    p=q[fi];
}

从队头去除元素的代码变成了二分查找,于是我们就可以解决这道题了。

5.剩下几道例题

单调队列优化DP:

例题

斜率优化DP:

例题1

例题2

斜率优化的状态转移方程比较方便推导,主要是难在对柿子的处理。

完结撒花

 
posted @ 2022-08-22 15:57  灵长同志  阅读(38)  评论(0)    收藏  举报