斜率優化dp 的簡單入門

不想寫什麼詳細的講解了...並且也以爲本身很難寫過某大佬(大米餅),因而建議把他的 blog 先看一遍,而後本身加了幾道題目以及解析...順便建議看看算法競賽(藍皮書)的 0x5A 斜率優化(P294) 部分php

這是——大米餅大佬html

 


 

看完了大米餅同志對斜率優化的介紹,下面我來稍微講講對斜率優化dp 的理解ios

 

前置知識

  1. 單調隊列(棧)
  2. 平面直角座標系
  3. 直線解析式
  4. 等式處理
  5. dp狀態設計
  6. balabala......

 

理解

其實斜率優化 dp 的原理很簡單:git

  1. 根據題目(斜率優化 dp 的題目通常都很裸)的 題意 以及 數據範圍 先根據設計一個狀態出來,當作 dp 數組。
  2. 根據已經設計好的 dp 狀態設計一個狀態轉移方程,若是設計失敗則跳回第一步,從新設計 dp 狀態。
  3. 在第二步完成後,咱們通常能夠獲得這樣的一個式子:$ Fi = Fj +$ $(一個表達式)$ 
  4. 根據設計的狀態作一些亂七八糟的東西(好比說將式子展開啦,移項啦...這些大米餅講的應該挺清楚的吧)。
  5. 在第四步中,咱們在將式子展開後,把下標中同時帶有 i、j 的項(注意這裏是項,如 Si*Sj)移到等式的左邊。
  6. 而後咱們就能夠獲得一個 $b + k x = y$ 形態的通常式(大米餅是這麼叫的),其中等式左邊下標帶有 i 的變量(注意這裏是變量,如Si)做爲 k ,下標帶有 j 的變量做爲 x ,等式右邊下標帶有 j 的變量做爲 y ,至於等式右邊那些下標只帶有 i 的變量咱們不去管它,由於 i 在當前對於 Fi 的計算中至關於一個常量,對結果的影響必定。
  7. 而後咱們發現這時候咱們就能夠獲得點 (x, y) 了,那麼咱們就基本上已經完成了這道 斜率優化 dp 題了。
  8. 那麼咱們是要根據題意讓 Fi (也就是通常式中的 b )取到最小(大)值,那麼咱們的任務也就轉化成了過一個點,畫一條斜率爲 k 的射線,令其與 y 軸的截距(與 y 軸的交點和原點的距離叫作截距)最小(大)。

作完這些工做以後,你把全部的點畫到平面直角座標系上,就能夠看到這樣的一條折線(假設咱們如今求的是 Fi 的最小值):算法

image

 

 

沒錯,這就是可能有用的點造成的像凸包同樣的東西(凸殼)。那爲何這些點造成的折線必定是向下凸起的呢?數組

咱們能夠想一想一下,若是折線內凹,那麼引發內凹的那個點可能成爲有用點嗎?不可能,由於通過該點的斜率爲 k 的直線與 y 軸的截距必然不會比它旁邊兩個點的截距要小優化

那麼咱們在來考慮一下,若是咱們要求的是 Fi 的最大值呢?那麼咱們只要讓折線向上凸起就行了(維護上凸性)。spa

對了對了!還有一點蠻重要的。那就是斜率優化可行性斷定的標準(我本身口胡的): 咱們在上面的步驟中會處理出一個通常式中的 k 嗎?那個 k 要知足單調性,設計

否則是沒辦法用雙端隊列(單調隊列)維護的。指針

 

而後這裏有一些題目

 

題目

洛谷P4072 [SDOI2016]征途 

分析

首先你在作這題以前最好已經作過了 洛谷P3648 [APIO2014]序列分割 這道題(上面的 T6)。

這道題你只要一直推推推把式子推出來,而後發現 $ans = m * \sum_{i=1}^{m}a[i]^{2} - sum[n]^{2}$ ,

那麼你就能知道咱們要求的是 $min{\sum_{i=1}^{m}a[i]^{2}}$ (這就是序列分割啊),最後答案處理一下就行了。

 

代碼

 1 //by Judge
 2 #include<iostream>
 3 #include<cstdio>
 4 #define ll long long
 5 using namespace std;
 6 const int M=3111;
 7 #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
 8 char buf[1<<21],*p1=buf,*p2=buf;
 9 inline int read(){
10     int x=0,f=1; char c=getchar();
11     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
12     for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
13 }
14 ll n,m,sum[M],g[M],f[M],q[M],head,tail;
15 inline double Rate(ll i,ll j){
16     if(sum[i]==sum[j]) return -1e9;
17     return (g[j]-g[i]+sum[j]*sum[j]-sum[i]*sum[i])/(1.0*sum[j]-sum[i]);
18 }
19 signed main(){
20     n=read(),m=read();
21     for(int i=1;i<=n;++i)
22         sum[i]=sum[i-1]+read(),g[i]=sum[i]*sum[i];
23     for(int k=2,i;k<=m;++k){
24         head=tail=0;
25         for(i=1;i<=n;++i){
26             while(head<tail && Rate(q[head+1],q[head])<=2*sum[i]) ++head;
27             f[i]=g[q[head]]+(sum[i]-sum[q[head]])*(sum[i]-sum[q[head]]);
28             while(head<tail && Rate(q[tail-1],q[tail])>=Rate(q[tail],i)) --tail; q[++tail]=i;
29         } swap(f,g);
30     } printf("%lld\n",g[n]*m-sum[n]*sum[n]); return 0;
31 }

 

洛谷P4360 [CEOI2004]鋸木廠選址 

分析

首先這道題其實與倉庫建設相似,同時(能夠算是)綜合了序列分割...可是數據的處理有點麻煩(甚至還有點坑,好比 d 和 w 搞反了而後樣例裏面 d 、w 讀入雷同...)

這題偷懶一點就是  綜合 一下序列分割這道題,將它做爲模板,K 設成 3 ,n 要加一,而後常規作就行了。推式子也不是很麻煩,和倉庫建設同樣的套路

 

代碼

 1 //by Judge
 2 #include<iostream>
 3 #include<cstdio>
 4 #define int long long
 5 #define ll long long
 6 using namespace std;
 7 const int M=5e4+111;
 8 #ifdef ONLINE_JUDGE
 9 #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
10 #endif
11 char buf[1<<21],*p1=buf,*p2=buf;
12 inline int read(){
13     int x=0,f=1; char c=getchar();
14     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
15     for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
16 }
17 ll n,K,x[M],w[M],s[M],g[M],f[M],q[M],head,tail;
18 inline double Rate(ll i,ll j){
19     if(w[i]==w[j]) return -1e18;
20     return (g[j]-g[i]+s[j]-s[i])/(1.0*w[j]-w[i]);
21 }
22 int las[M];
23 signed main(){
24     n=read()+1;
25     for(int i=1;i<=n;++i)
26         (i<n)&&(w[i]=read(),x[i+1]=x[i]+read()),
27         s[i]=s[i-1]+x[i]*w[i],w[i]+=w[i-1],g[i]=1e18;
28     for(int k=1,i;k<=3;++k){
29         head=tail=0;
30         for(i=1;i<=n;++i){
31             while(head<tail && Rate(q[head+1],q[head])<=x[i]) ++head;
32             f[i]=g[q[head]]+x[i]*(w[i-1]-w[q[head]])-s[i-1]+s[q[head]];
33             while(head<tail && Rate(q[tail-1],q[tail])>=Rate(q[tail],i)) --tail; q[++tail]=i;
34         } swap(f,g);
35     } printf("%lld\n",g[n]); return 0;
36 }

 

其餘題...咳咳(來自刷題慢者的尷尬...作一道題都要在黑板上墨跡個半天QwQ)

洛谷P2365 任務安排 

分析

這道題...其實蠻(不)簡單的啦。這題你要消除後效性才能作(其實這是一道經典題目,算法競賽上都有)。

一開始咱們讓 $1~n$ 這一整段爲一個區間,如今咱們考慮當前將$ 1 ~ i $這一區間分爲一段,

那麼後面全部的任務(以及 $1 ~ i$ 這段區間)一定會多加上 S 時間的代價(即 $S * (C_{n}-C_{1})$,其中 C 爲 c 數組的前綴和),然後面區間的分割並不會對前面的分割形成影響。

同理, i 以後的區間咱們也能夠按照這個思路分下去,以消除後效性。那麼咱們如今就能夠列出狀態轉移方程:$ Fi = Fj + (Cj-Ci)*xi + (Cn-Cj)*S $ 而後展開式子移項後咱們就能夠斜率優化了。

 

代碼

 1 //by Judge
 2 #include<iostream>
 3 #include<cstdio>
 4 #define mid (l+r>>1)
 5 #define ll long long
 6 using namespace std;
 7 const int M=1e6+111;
 8 #ifdef online_judge
 9 #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
10 #endif
11 char buf[1<<21],*p1=buf,*p2=buf;
12 inline int read(){
13     int x=0,f=1; char c=getchar();
14     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
15     for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
16 }
17 ll n,S,head,tail,q[M],t[M],c[M],f[M];
18 inline double X(int i){ return c[i]; }
19 inline double Y(int i){ return f[i]-c[i]*S; }
20 inline double Rate(int i,int j){ return (Y(j)-Y(i))/(X(j)-X(i)); }
21 signed main(){
22     n=read(),S=read();
23     for(int i=1;i<=n;++i) t[i]=t[i-1]+read(),c[i]=c[i-1]+read();
24     for(int i=1;i<=n;++i){
25         while(head<tail && Rate(q[head+1],q[head])<=t[i]) ++head;
26         f[i]=f[q[head]]+(c[n]-c[q[head]])*S+(c[i]-c[q[head]])*t[i];
27         while(head<tail && Rate(q[tail],q[tail-1])>=Rate(i,q[tail])) --tail; q[++tail]=i;
28     } printf("%lld\n",f[n]); return 0;
29 }

 

洛谷CF311B Cats Transport 

分析

沒什麼好分析的和上面同樣公式套取就行了(套個鬼哦)。

咳咳。首先你要分析怎麼把這道題硬設計出 dp 狀態。那麼咱們來看看,其實路程對於問題的影響並不顯得有多麼重要,因而咱們能夠考慮消除這個影響。

如何消除?題目中說,飼養員(說好的鏟屎官...)到達貓的位置所需時間就是距離 $X_{i}$,那麼其實貓在 $T_{i}$ 的時間開始等待,而飼養員出發時間對於咱們要求的答案是沒有影響的(況且題目中說了飼養員出發時間能夠爲負數),

那麼咱們可讓貓開始等待的時間 $T_{i}$ 減去路程的影響 $X_{i}$ (感性理解一下),而後咱們再對減完 $X_{i}$ 的 $T_{i}$ 排一下序就行了。

這裏如何解釋?emmm...思考一下,一個飼養員出發必然是會接回全部正在等待的貓對吧(貓不可能給下一我的接,那樣不會更優,而上一我的能接回去早接回去了)

咳咳...那麼這個飼養員能接到哪些貓呢?固然是 $T-X$(等待開始時間減去路程) 小於等於飼養員出發時間的全部貓咯!因而解釋完畢。

排完序後,能夠看出咱們要接到第 i 只貓的話,它前面的貓咱們均可以接到(由於在這隻貓以前的貓早就開始等待了)。

因而這道題就變成了分割序列...(切 p 刀,可是注意這裏是最多切 p 刀,不必定切完,有的飼養員能夠不動的嘛)。

咳咳,可是轉移方程是不同的:$ F_{i} = F_{j} + (T_{i}-T_{i-1}) * (i-j) - (T_{i}-T_{j}) $  (T 是上文中 $T-X$ 的前綴和數組)

那麼這個表達式原來的樣子是:$ F_{i} = MIN{ F_{j} + \sum_{k=j}^{i} ( t_{i}-t_{k} )  }$ (這裏的 t 是上文中的 $T-X$ 數組)

而後咱們就很是愉快的 ctrl+C 、 ctrl+V 將以前打好的序列分割板子弄了下來開始了新一輪的斜率優化。

 

代碼

 

 1 //by Judge
 2 #include<algorithm>
 3 #include<iostream>
 4 #include<cstdio>
 5 #define ll long long
 6 using namespace std;
 7 const int M=1e5+111;
 8 const ll inf=1e16+7;
 9 #ifdef ONLINE_JUDGE
10 #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
11 #endif
12 char buf[1<<21],*p1=buf,*p2=buf;
13 inline int read(){
14     int x=0,f=1; char c=getchar();
15     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
16     for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
17 }
18 ll n,m,p,ans=inf,d[M],a[M],g[M],f[M],q[M],head,tail;
19 inline double Y(int i){ return g[i]+a[i]; }
20 inline double X(int i){ return i; }
21 inline double Rate(ll i,ll j){ return (Y(j)-Y(i))/(X(j)-X(i)); }
22 signed main(){
23     n=read(),m=read(),p=read();
24     for(int i=2;i<=n;++i) d[i]=d[i-1]+read();
25     for(int i=1,x,y;i<=m;++i) x=read(),y=read(),a[i]=y-d[x],g[i]=inf;
26     sort(a+1,a+m+1); for(int i=2;i<=m;++i) a[i]+=a[i-1];
27     for(int k=1,i;k<=p;++k){
28         head=tail=0;
29         for(i=1;i<=m;++i){
30             while(head<tail && Rate(q[head+1],q[head])<=a[i]-a[i-1]) ++head;
31             f[i]=g[q[head]]+(a[i]-a[i-1])*(i-q[head])-a[i]+a[q[head]];
32             while(head<tail && Rate(q[tail-1],q[tail])>=Rate(q[tail],i)) --tail; q[++tail]=i;
33         } swap(f,g); ans=min(ans,g[m]);
34     } printf("%lld\n",ans); return 0;
35 }

 

BinamotoOJ  P4709: [Jsoi2011]檸檬

分析

這題就是斜率優化裸題啊! 這道題很是值得一作,由於它深入的告訴了咱們,斜率優化能夠用單調棧維護!

首先這題就是讓咱們吧一個序列分紅若干份,而後根據公式計算最大值(注意是最大值,維護折線上凸性)

咱們能夠很是輕鬆的看出咱們要取的一段區間的左右端點必然是相同的顏色,且咱們選擇的顏色就是左右端點的顏色。

(咱們每一段只能選一種顏色,那麼若是左右端點不一樣,咱們徹底能夠將與選擇的顏色不一樣的那一端隔離出來分到另外一段區間裏,那樣更優)

因而咱們在讀入時維護一個 las 指針,指向當前顏色上一次出現的位置,同時記錄每一個點以前與該點顏色相同的點有多少個(用 S 數組記錄)。

而後咱們將全部顏色第一次出現時的位置壓入單調棧,接着就能夠開始 dp 了。

那麼 dp 轉移式就是 : $$ f[i] = f[j-1] + (s[i]-s[j]+1)*a[i] $$ (其中 i 、j 位置的貝殼顏色相同)

斜率式就是: $$ f[i] + 2*a[i]*s[i]*s[j] = f[j-1] + a[i]*s[j]^{2} - 2*a[i]*s[j] + 2*a[i]*s[i]+a[i]*s[i]^{2}+a[i] $$

$$ X(i)= s[j] , K=2*a[i]*s[i] $$

$$ Y(i)= f[j-1] + a[i]*s[j]^{2} - 2*a[i]*s[j] $$

 

代碼

 1 //by Judge
 2 #include<iostream>
 3 #include<cstdio>
 4 #include<queue>
 5 #define ll long long
 6 using namespace std;
 7 const int M=1e5+111;
 8 //#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
 9 char buf[1<<21],*p1=buf,*p2=buf;
10 inline int read(){
11     int x=0,f=1; char c=getchar();
12     for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
13     for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f;
14 }
15 ll n,ans,las[M],a[M],s[M],f[M],top[M]; vector<int> q[M];
16 inline long double X(int i){ return s[i]; }
17 inline long double Y(int i){ return f[i-1]+a[i]*s[i]*(s[i]-2); }
18 inline long double Rate(ll i,ll j){ return (Y(j)-Y(i))/(X(j)-X(i)); }
19 signed main(){
20     n=read(); ll p,x,y;
21     for(int i=1;i<=n;++i) a[i]=read(),s[i]=s[las[a[i]]]+1,las[a[i]]=i;;
22     for(int i=1;i<=n;++i) if(las[a[i]]) q[a[i]].push_back(i),las[a[i]]=0;
23     for(int i=1;i<=n;++i){ p=a[i];
24         while(top[p]>1 && Rate(q[p][top[p]-1],q[p][top[p]])<=Rate(q[p][top[p]],i)) --top[p],q[p].pop_back();
25         ++top[p],q[p].push_back(i);
26         while(top[p]>1 && Rate(q[p][top[p]-1],q[p][top[p]])<=2*p*s[i]) --top[p],q[p].pop_back();
27         f[i]=f[q[p][top[p]]-1]+(s[i]-s[q[p][top[p]]]+1)*(s[i]-s[q[p][top[p]]]+1)*p;
28     } printf("%lld\n",f[n]); return 0;
29 }
相關文章
相關標籤/搜索