史上最詳盡的斜率優化!

最近被欽定要寫教材,負責斜率優化那一塊,就把寫的內容搬了些上來。

3.6.1 斜率優化DP的基本思想

考慮這樣一個問題:現在要給n個數a[1],a[2]…a[n]分組,每分出一組,你的代價爲該組所有數的和的平方+一個常數M(即設你分出的一組數字是2,3,4,M=5,則你分出該組的代價爲(2+3+4)^2+5),求出一種分組方式使得代價總和最小,輸出這個最小的代價和。

 

如果是最基礎的dp,能夠很顯然地列出來:設dp[i]爲將1~i分組的最小代價和,s[i]爲a[1]+a[2]..+a[i]的和。dp[i]=Min(f[j]+(s[i]-s[j])^2+M),(0<=j<i)。但是這樣dp的時間複雜度是O(n^2)的,如果n很大(比如說n=100000),這樣的時間複雜度我們是接受不了的,我們需要尋求優化。

 

我們想,在轉移時我們要花費大量時間枚舉j,這很不划算,我們能否用O(1)或者O(logn)的時間尋找到所有轉移中最優的那個j呢?

我們假設在求解dp[i]的時候,有j,k(j>k)使得從j轉移比從k轉移更優,那麼需要滿足條件:

dp[j]+(s[i]s[j])^2+M<dp[k]+(s[i]s[k])^2+M

展開上式,移項並消去同類項可得:

(dp[j]dp[k]+s[j]^2s[k]^2)/(S[j]S[k])<2*s[i]

我們設f[j]=dp[j]+s[j]^2,f[k]同理,則可得:(f[j]f[k])/(s[j]s[k])<2*s[i]

也就是說當j>k時,若有上式,則用j更新dp[i]比用k更新dp[i]優。通過這樣我們已經能夠比較兩個點在轉移i的時候的優劣性了。

 

現在我們令g(j,k)=(f[j]-f[k])/(s[j]-s[k]),上式即轉換爲若g(j,k)<2*s[i],則j比k優,反之k比j優。有一個結論是:

設x爲大於i的一個點,如果g(j,k)>g(i,j)(k<j<i),則無論如何j都不可能再成爲一個決策點了(也就是說對於以後每個dp[x],都不可能由dp[j]轉移而來)。

我們來嘗試證明它:

g(j,k)和g(i,j)和2*s[x]有三種關係,即g(j,k)>g(i,j)>2*s[x],g(j,k)> 2*s[x]>g(i,j),2*s[x]>g(j,k)>g(i,j),我們分情況討論:對於第一種關係,j比i優,但k比j優,所以j不可能成爲最優決策點。對於第二種關係,i比j優,k比j優,所以j不可能成爲最優決策點。對於第三種關係,i比j優,j比k優,所以j不可能成爲最優決策點。 綜上所述,我們可以斷言:只要存在g(j,k)>g(i,j),(k<j<i),那麼j無論如何都不可能再成爲最優決策點。

我們來想一想怎麼運用這個性質來優化dp。我們建立一個隊列d表示可能的決策點集合,每次我們算出了dp[i]的值,便可以知道f[i]的值了。我們將i加入這個集合,根據上述的性質,我們可以用i刪去這個集合中的某些點。具體來說,就是刪去所有g(j,k)>g(i,j)的j。那我們難道要枚舉每個j,k把它們嘗試刪除嗎?這樣太慢了,我們再來想辦法。

我們每次這樣維護,可以知道將隊列d中的點映射到平面上(以s爲橫座標,以f爲縱座標),它們依次的連線是下凸的(因爲g(j,k)的本質就是斜率),如圖所示:

 

有着這樣一個性質,每次我們在隊列末尾插入一個點i,就只需要檢驗在隊列中i,與i前面一個位置j,與j前面一個位置k(這裏所說的位置都是指在隊列中的位置),是否是呈下凸的,如果不呈,則將j刪去後繼續檢驗,直到呈下凸爲止(這個用while循環可以實現)。

這樣,我們就得到一個由所有可能決策點所組成的隊列d,如果我們現在要求dp[i],我們應該由隊列d中的哪個來轉移過來呢?

我們先回想一下開始得到的結論:對於k<j<i,如果g(j,k)<2*s[i],則j比k優,反之k比j優。

         我們現在知道集合d是下凸的,也就是g的值是逐漸遞增的,我們需要尋找一個最大的g(j,k),使得g(j,k)<2*s[i],那麼這個j就是最優的決策點。證明:因爲g(j,k)<2*s[i],所以對於i來說j比k優,因爲g遞增,所以對於k後一個位置l,g(k,l)<g(j,k)<2*s[i],所以k比l優,以此類推對於i來說,j應爲最優的決策點。因爲g的遞增性,尋找最大的g(j,k)<2*s[i],可以用二分法,總的時間複雜度就可以優化成O(nlogn),這是一般的斜率優化的解法。但注意到這題有個性質,就是s[i]也是遞增的,這樣我們就可以像單調隊列那樣來找到最優決策點,而不用二分,這時的時間複雜度就是O(n)的了。

         那麼我們關於斜率優化DP的思想的學習到這裏基本就結束了,我們現在來幫助大家總結一下整個過程,希望以此來幫助大家徹底掌握這個知識點。

         1.先列出一個最基礎的dp;如果這個dp不涉及最小最大值轉移,或對轉移有較多複雜的限制條件,則無法用斜率優化。

2.令k<j<i,考慮對於i來說j比k優的情況,列出式子,並轉換爲一個形如(f[j]-f[k])/(s[j]-s[k])<(或>)s[i]的式子(這個大於小於號是根據題目是求最小值還是最大值確定的)。如果轉化不了說明這個dp不能用斜率優化。

3.得到結論當g(j,k)<(或>)g(i,j)時,j不再可能是一個最優決策點。

4.對於可能的決策點隊列d,根據結論維護上凸或下凸,即每次加入一個點就刪去一些點。

5.在隊列d中,二分查找一個斜率小於s[i]且斜率最大的點(或斜率大於s[i]且斜率最小的點,看不等式符號決定),作爲dp[i]的轉移點。特殊地,如果s[i]單調遞增遞減,還可以用單調隊列維護。

 

 

3.6.2 斜率優化DP的應用

例3.6-1 hdu 3507 Print Article:

【參考程序】

#include<cstdio>

#define N 100010

int a[N],d[N],s[N],dp[N]; //字母的定義與上文相同

int y(int x,int y)   //將相鄰點映射到平面上的縱座標之差

{

         return dp[y]-dp[x]+s[y]*s[y]-s[x]*s[x];

}

int x(int x,int y)  //將相鄰點映射到平面上的橫座標之差

{

         return s[y]-s[x];

}

int main()

{

         int n,M;

         scanf("%d%d",&n,&M);

       for (int i=1;i<=n;i++)

         {

                scanf("%d",&a[i]);

                s[i]=s[i-1]+a[i];

         }

         int l=0; int r=0;

         for (int i=1;i<=n;i++)

         {

                   int k=2*s[i];

                   while (l<r && y(d[l],d[l+1])<=k*x(d[l],d[l+1])) l++; //單調隊列,找出斜率小於k的最大值(爲了保證精度這裏用乘法代替除法)

                   dp[i]=dp[d[l]]+x(d[l],i)*x(d[l],i)+M;

                   while (l<r && y(d[r-1],d[r])*x(d[r],i)>=y(d[r],i)*x(d[r-1],d[r])) r--; //每加入一個點,爲維護上凸或下凸的性質要刪去一些點

                   r++; d[r]=i;

         }

         printf("%d\n",dp[n]);

         return 0;

}

 

例3.6-2:[APIO2010] 特別行動隊

【題目描述】

  你有一支由n名預備役士兵組成的部隊,士兵從1到n編號,要將他們拆分成若干特別行動隊調入戰場。出於默契考慮,同一支特別行動隊中隊員的編號應該連續,即爲形如(i,i+1,…,i+k)的序列。

  編號爲i的士兵的初始戰鬥力爲xi,一支特別運動隊的初始戰鬥力x爲隊內士兵初始戰鬥力之和,即x=(xi)+(xi+1)+…+(xi+k)。

  通過長期的觀察,你總結出一支特別行動隊的初始戰鬥力x將按如下經驗公式修正爲x’:x’=ax^2+bx+c,其中a,b,c是已知的係數(a<0)。

  作爲部隊統帥,現在你要爲這支部隊進行編隊,使得所有特別行動隊修正後戰鬥力之和最大。試求出這個最大和。

  例如,你有4名士兵,x1=2,x2=2,x3=3,x4=4。經驗公式中的參數爲a=-1,b=10,c=-20。此時,最佳方案是將士兵組成3個特別行動隊:第一隊包含士兵1和士兵2,第二隊包含士兵3,第三隊包含士兵4。特別行動隊的初始戰鬥力分別爲4,3,4,修正後的戰鬥力分別爲4,1,4。修正後的戰鬥力和爲9,沒有其它方案能使修正後的戰鬥力和更大。

【輸入格式】

輸入由三行組成。第一行包含一個整數n,表示士兵的總數。第二行包含三個整數a,b,c,經驗公式中各項的係數。第三行包含n個用空格分隔的整數x1,x2,…,xn,分別表示編號爲1,2,…,n的士兵的初始戰鬥力。

【輸出格式】

輸出一個整數,表示所有特別行動隊修正戰鬥力之和的最大值。

【樣例輸入】

4

-1 10 -20

2 2 3 4

【樣例輸出】

9

【數據範圍】

20%的數據中,n<=1000;

50%的數據中,n<=10000;

100%的數據中,1<=n<=1000000,-5<=a<=-1,b<=10000000,|c|<=10000000,1<=xi<=100。

 

【問題分析】

容易寫出dp方程:dp[i]=max(dp[j]+a*(s[i]−s[j])^2+b*(s[i]−s[j])+c)。考慮斜率優化。我們回憶一下上文的步驟,令k<j<i,考慮對於i來說j比k優的情況,列出式子dp[j]+a*(s[i]−s[j])^2+b*(s[i]−s[j])+c> dp[k]+a*(s[i]−s[k])^2+b*(s[i]−s[k])+c,令f[x]=dp[x]+a*s[x]^2,則可得(f[j]-f[k])/(s[j]-s[k])>2*a*s[i]+b。令g(j,k)=(f[j]-f[k])/(s[j]-s[k]),則若g(j,k)>2*a*s[i]+b,則對於i來說j比k優,反之相反。可得結論若有g(j,k)<g(i,j),(其中k<j<i),則j不再可能成爲一個決策點。我們維護可能的決策點隊列d,根據結論可知將s作爲橫座標,f作爲縱座標,映射到平面上,是要維護一個上凸的圖形(斜率要遞減)。每次加入一個點到隊列d中,爲了維護上凸的性質就會刪去一些點。然後我們在隊列d中,用單調隊列維護一個斜率大於2*a*s[i]+b且斜率最小的點,作爲dp[i]的轉移點。

 

【參考程序】

#include<cstdio>

long long s[1000010],p[1000010];

long long f[1000010];

int d[1000010];

int main()

{

         int n;

         scanf("%d",&n);

         long long a,b,c;

         scanf("%lld%lld%lld",&a,&b,&c);

         int x;

         for (int i=1;i<=n;i++) scanf("%d",&x),s[i]=s[i-1]+x;//s是前綴和

         f[0]=0;

         int head=1; int tail=1; d[1]=0;

         for (int i=1;i<=n;i++)

         {

                   while (head<tail && p[d[head+1]]-p[d[head]]>(2*a*s[i]+b)*(s[d[head+1]]-s[d[head]])) head++;//單調隊列找出轉移點

                   int j=d[head];

                   f[i]=f[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c;//這裏的f相當於分析中的dp數組

                   p[i]=f[i]+a*s[i]*s[i];//這裏的p相當於分析中的f數組

                   while (tail>head && (p[d[tail]]-p[d[tail-1]])*(s[i]-s[d[tail]])<(p[i]-p[d[tail]])*(s[d[tail]]-s[d[tail-1]])) tail--;//爲維護上凸,將一些點刪去

                   d[++tail]=i;

         }

         printf("%lld\n",f[n]);

         return 0;

}

 

3.6.3 斜率優化習題推薦

1.[HNOI2008] 玩具裝箱

【題目描述】

  P教授要去看奧運,但是他舍不下他的玩具,於是他決定把所有的玩具運到北京。他使用自己的壓縮器進行壓縮,其可以將任意物品變成一堆,再放到一種特殊的一維容器中。P教授有編號爲1…N的N件玩具,第i件玩具經過壓縮後變成一維長度爲C[i].爲了方便整理,P教授要求在一個一維容器中的玩具編號是連續的。同時如果一個一維容器中有多個玩具,那麼兩件玩具之間要加入一個單位長度的填充物,形式地說如果將第i件玩具到第j個玩具放到一個容器中,那麼該容器的長度將爲 x=j-i+C[k],(i<=K<=j)

  製作容器的費用與容器的長度有關,根據教授研究,如果容器長度爲x,其製作費用爲(x-L)^2.其中L是一個常量。P教授不關心容器的數目,他可以製作出任意長度的容器,甚至超過L。但他希望費用最小.

【輸入格式】

第一行輸入兩個整數N,L,第二行輸入所有C[i],1<=N<=50000,1<=L,C[i]<=10^7

【輸出格式】

一行,輸出最小費用

【樣例輸入】

5 4

3 4 2 1 4

【樣例輸出】

1

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章