1. 程式人生 > >[模板] 斜率優化dp詳解

[模板] 斜率優化dp詳解

演算法簡介

今天xinyue講了斜率優化,全程懵逼,居然還有這麼牛逼的東西。 
於是與achen討論了一下,總結一些東西。 
斜率優化Dp其實是單調佇列的推廣,單調佇列、旋轉卡殼、斜率優化都利用了單調性降低時間複雜度。

演算法簡介

舉個例子 
有些動規狀態轉移方程可以寫成 
f[i]=min/max{f[j]+…+x[i]},省略號中只有與j有關的變數。 
我們就可以用單調佇列進行優化,使O(n^2)降為O(n) 
但是對於這一類的方程: 
f[i]=min/max{f[j]+(x[i]-x[j])^2} 
展開後得到 
f[i]=min/max{f[j]+x[i]^2+x[j]^2-2*x[i]*x[j]


紅色部分不僅有j相關的量,還有與i有關的量,從而使單調佇列失效。 
此時我們就需要用到斜率優化。

引例

hdu3507 
題目大意: 
給出N個單詞,每個單詞有個非負權值Ci,現要將它們分成連續的若干段,每段的代價為此段單詞的權值和,還要加一個常數M,即(∑Ci)^2+M。現在想求出一種最優方案,使得總費用之和最小。

容易寫出方程: 
f[i]=min{f[j]+(s[i]-s[j])^2+M}(0<=j<=i-1) 
其中s是字首和 
可是範圍是500000,又不能用單調佇列,那怎麼辦呢?

演算法核心

數學分析法見此大爺部落格,講的挺詳細->傳送門 
以下談談斜率優化的數形結合理解方法: 
以下純粹空談,請結合引例分析理解。

根據動規方程狀態i從狀態j轉化而來, 
我們可以化成類似f[i]=(f[j]+…)+(-i*f[i-1]*f[j])+(i+…)的形式 
其中藍色部分只與j有關紅色部分與i,j有關綠色部分只與i或常數有關 
我們可以固定i,故變形為 
f[i]-i-…=(f[j]+…)+(-i*f[i-1]*f[j]) 
考慮幾何意義, 
藍色部分為y紅色部分中的i部分為導數k,紅色部分中j部分為x綠色部分只是常數,在幾何意義上只是截距,與單調性無關,可以設為B 
故得y=kx-B 
是不是很像直線方程? 
假設關於i的部分>0且隨著i增加而增加,求的是最小值 
則k隨著i增加而增加,對於有效點集我們可以畫出下圖。 
這裡寫圖片描述


是不是很像一個下凸包? 
我們用當前的斜率k從下方去不斷逼近下凸包,最終會先碰到哪一個點? 
這裡寫圖片描述
一定是與斜率最相近的點,因為函式單調遞增,故小於斜率的決策肯定沒有後面的優,捨去。 
因此我們可以用一個類似單調佇列的雙端佇列來維護狀態j,以下是實現方案:

若導數小於當前斜率,舍掉隊首。 
根據方程使用隊首取出j算出當前f[i]的值 
接著我們要加入結點i,但還得維護佇列的下凸性,如果加入結點i破壞了下凸性,就彈去隊尾,直到下凸位置(請dalao們不要吐槽示意圖,沒時間改了) 
這裡寫圖片描述
因此可以得到O(n)的演算法

當然,若方程關於i的部分隨著i增加而減小,且>0,則維護上凸性。以此類推,樹形結合分析。

核心程式碼如下:

int Left=1,Right=1;
    Q[1]=0;
    f[0]=0;
    for(int i=1; i<=n; i++) {
        while(Left<Right&&Slope(Q[Left],Q[Left+1])<=sumd[i])Left++; //維護隊首(刪除非最優決策)
        int Front=Q[Left]; //取出佇列中最優元素j 
        f[i]=Cal(i,Front); //根據方程計算當前f
        while(Left<Right&&Slope(Q[Right-1],Q[Right])>=Slope(Q[Right],i))Right--; //維護隊尾(維護下凸包性質)
        Q[++Right]=i; //入隊
    }

引例分析

f[i]=min{f[j]+(s[i]-s[j])^2+M} 
展開得 
f[i]=min{f[j]+s[i]^2+s[j]^2-2*s[i]*s[j]+M} 
令f[i]=B,f[j]+s[j]^2=y,2*s[j]=x,k=s[i] 
因此k*x+B=y 
k是s[i],字首和隨著i增大而增大,因為求最小值,故維護下凸包。

#include<algorithm>
#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<vector>
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
inline const int Get_Int() {
    int num=0,bj=1;
    char x=getchar();
    while(x<'0'||x>'9') {
        if(x=='-')bj=-1;
        x=getchar();
    }
    while(x>='0'&&x<='9') {
        num=num*10+x-'0';
        x=getchar();
    }
    return num*bj;
}
long long n,m,Q[500005],f[500005],s[500005];
double Slope(long long j,long long k) { //求斜率 
    return double((f[j]+s[j]*s[j])-(f[k]+s[k]*s[k]))/(2*s[j]-2*s[k]);
}
int main() {
    while(scanf("%lld%lld",&n,&m)!=EOF) {
        for(int i=1; i<=n; i++)s[i]=s[i-1]+Get_Int();
        int Left=1,Right=1;
        Q[1]=0;
        f[0]=0;
        for(int i=1; i<=n; i++) {
            while(Left<Right&&Slope(Q[Left],Q[Left+1])<=s[i])Left++; //維護隊首(刪除非最優決策) 
            int Front=Q[Left];
            f[i]=f[Front]+(s[i]-s[Front])*(s[i]-s[Front])+m; //計算當前f 
            while(Left<Right&&Slope(Q[Right-1],Q[Right])>=Slope(Q[Right],i))Right--; //維護隊尾(維護下凸包性質) 
            Q[++Right]=i; //入隊 
        }
        printf("%lld\n",f[n]);
    }
    return 0;
}

經典例題

這裡留下的坑慢慢填吧,希望填的完。

後記

斜率優化這一部分比較難懂,讀者可以自己在紙上推算一下。 
如果有疑問或者認為本文有問題請在下面↓提出,感謝大家的支援。