1. 程式人生 > 實用技巧 >斜率優化DP複習筆記

斜率優化DP複習筆記

斜率優化DP複習筆記

前言

複習筆記2nd。

Warning:鑑於擺渡車是普及組題目,本文的難度定位在普及+至省選-。

參照洛谷的題目難度評分(不過感覺部分有虛高,提高組建議全部掌握,普及組可以選擇性閱讀。)

引用部分(如這個文字)為總結性內容,建議即使是跳過部分也進行閱讀。

0——P3195[HNOI2008]玩具裝箱

題目連結
怎麼一上來就是紫題啊

題意

給定 \(C_i\) 表示每個物體長度,把 \(i\sim j\) 的物品放入一個容器中,容器的長度為 \(x=j-i+\sum_{k=i}^j C_k.\) 一個長度為 \(x\) 的容器製作費用為 \((x-L)^2\) ,求裝下所有物品的最小費用。

思路

\(S[n]=\sum( C_i+1),f[i]\) 為裝好前 \(i\) 個的最小花費,轉移方程為 :

\[f[i]=min(f[j]+(S[i]-S[j]-1-L)^2). \]

\(L\) 提前加一,去掉 \(min\) 化簡得:

\[f[i]=f[j]+(S[i]-S[j]-L)^2 \]

把平方拆開得到:

\[f[i]=S[i]^2-2S[i]L+f[j]+(S[j]+L)^2-2S[i]S[j] \]

下面將描述如何進行斜率優化。


斜率優化的一般方式

注:此處對應講解的“線性規劃”部分,個人認為比較便於理解。

對於上面那個式子,進行移項,使得變成形如 \(y=kx+b\)

的形式。

移項遵循原則:把含有 \(g(i)\times g(j)\) 的表示式看做斜率 \(k\) 乘以未知數 \(x\) ,含有 \(f[i]\) 的項必須在 \(b\) 的表示式中,含有 \(g(j)\) 的項必須在 \(y\) 的表示式中。為了方便分析,如果 \(x\) 的表示式單調遞減,等式兩邊同乘 \(-1\) 變為單增。

那麼原式就可以化為:

\[2(S[i])S[j]+(f[i]-S[i]^2+2S[i]L)=(f[j]+(S[j]+L)^2) \]

其中,一次函式的各個項分別對應:

\[k_i=2S[i],x_i=S[j],b_i=f[i]-S[i]^2+2S[i]L,y_i=f[j]+(S[j]+L)^2 \]

對於這個式子,我們的目的是求出一個 \(j\) 使得 \(f[i]\) 最小,又有 \(b[i]=f[i]-S[i]^2\) ,所以從影象角度看,就是找某個點使得這條直線經過它的時候算出來的 \(b\) 最小。

由上面的式子可以知道,這條直線的斜率是固定的。那麼想象在一個平面上,有一些點,一條直線向上移,碰到的第一個點一定是使得 \(b\) 最小的位置。 可以發現,可能的點位於點集的下凸包上。

那麼現在只需要考慮如何維護凸包點集即可。

用單調佇列維護:

(1) 在凸包上找到最優點 \(j\) ,並用來更新 \(f[i]\)

(2) 將 \(i\) 作為一個決策點加入,並更新凸包(如果點 \(i\) 也是決策點之一,那麼交換順序)。具體操作為,(對於下凸包)如果 \((q[t-1],q[t])\) 的斜率不大於 \((q[t],i)\) 的斜率,那麼隊尾出隊,出隊完成後把 \(i\) 加入。
slope(q[t-1],q[t])>=slope(q[t-1],i) (這個地方講義裡面貌似有誤,和下凸包的情況不符)


決策單調性再優化

Warning :此部分需要證明,並非通用。

\(j_0[i]\)\(f[i]\) 轉移的最優決策點,那麼有 \(\forall i\leq i',j_0[i]\leq j_0[i']\) (非嚴格遞增)(證明略,見文末講義)

考慮如何證明。此題中 \(k_0[i]=2S[i]\) 顯然單增。詳細一點就是:\(k_0[i]\) 單增,最優決策點單增(看下凸包的圖)。當然如果不敢肯定的話還是老老實實二分棧吧qwq

由於最優決策點遞增,那麼可以用單調佇列維護。在原先找 \(j\) 的過程中,是從隊頭一個一個找(或者二分)的,現在就改為:
如果隊頭線段斜率 \(\leq k_0[i]\) 直接出隊,停止時即為最優決策點。

複雜度 \(O(nlogn)=>O(n)\)


程式碼

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=5e4+10;
ll n,L,h=1,t=0,q[N],s[N],f[N];
ll X( ll num ) { return s[num]; }
ll Y( ll num ) { return f[num]+(s[num]+L)*(s[num]+L); }
lb slope( ll n1,ll n2 ) { return (lb)(Y(n2)-Y(n1))/(X(n2)-X(n1)); }

int main()
{
        scanf( "%lld%lld",&n,&L ); L++; s[0]=0;
        for ( int i=1; i<=n ;i++ )
                scanf( "%lld",&s[i] ),s[i]+=s[i-1]+1;
        
        q[++t]=0;
        for ( int i=1; i<=n; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])<=2*s[i] ) h++;
                f[i]=f[q[h]]+(s[i]-s[q[h]]-L)*(s[i]-s[q[h]]-L);
                while ( h<t && slope(q[t-1],q[t])>=slope(q[t-1],i) ) t--;
                q[++t]=i;
        }

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

1——關於單調性的研究

\(X(j)\) 單增單減

將方程變為 \(\dfrac{Y(j_2)-Y(j1)}{X(j_2)-X(j_1)}\leq k_0[i]\) (或者大於等於)或者 \(kx+b=y\) 的形式。

注意遵循之前的原則。

決策點橫座標 \(X(j)\) 不單調

假設此時 \(k_0[i]\) 仍然單調。

那麼維護凸包不能用單調隊列了(因為會插入到點集中間某一個位置)。需要用到 平衡樹維護 或者 CDQ分治 。

斜率 \(k_0[i]\) 不單調

仍然可以佇列維護,但是不再滿足之前的決策單調性。那麼只能使用優化前的方法,二分佇列找最優決策點。

以上二者均不單調

考慮在第二種情況上進行改進。

由於平衡樹支援查詢前驅後繼,直接 \(k_0[i]\) 扔進去即可。

再看看 CDQ;在 第二種情況上再加一維偏序,人為排出單調性,普通單調佇列維護即可。

好了。東西講完了。上菜——

2——[NOIP2018PJ] P5017 擺渡車

題目連結

題意

\(n\) 名同學要乘坐擺渡車從 \(A\)\(B\),第 \(i\) 位同學在第 \(t_i\) 分鐘去等車。有一輛擺渡車,容量無限大,從 \(A\) 出發、 把車上的同學送到 \(B\)、再回到 \(A\)(去接其他同學),往返一趟總共花費 \(m\) 分鐘(上下車時間忽略不計)。要將所有同學都送到 \(B\).

已知你可以任意安排擺渡車出發的時間,求所有人等車時間之和的最小值。擺渡車回到 \(A\) 後可以即刻出發。

\(n\leq 500,m\leq 100,0\leq t\leq 4e6\)

思路

\(f_i\) 表示到了時間點 \(i\),所有同學等待時間最小和。可以寫出方程:

\[f_i=min( f_j+\sum_{j<t_k<\leq i} (i-t_k) ),j\leq i-m \]

字首優化。設 \(cnt_i\)\(i\) 時刻到達車站的學生個數,\(sum_i\) 表示已經到達車站的學生到達時間總和。有方程:

\[f_i=min( f_j+(cnt_i-cnt_j)\times i-(sum_i-sum_j)),j\leq i-m \]

看到了 \(i,j\) 乘積!好,上斜優。首先來整理一下式子:

\[f_i=min( f_j+sum_j-cnt_j\times i )+cnt_i\times i-sum_i \]

\(min\) 去掉,轉化成 \(kx+b=y\) 的形式:

\[f_j+sum_j=cnt_j\times i+f_i-cnt_i\times i+sum_i \]

\(i\) 作為 \(k\)\(cnt_j\) 作為 \(x\),剩餘為 \(b\) ,得到了直線方程。決策單調性轉移即可。

但是這題還有一些問題需要處理:

  1. \(j\leq i-m\) 。採用分層入隊,每當完成了 \(f_i\) 的轉移,就把 \(i-m+1\) 這個決策點入隊即可。
  2. 有重點。也就是橫座標相等縱座標不等,那麼按位置判斷 \(inf/-inf\) 即可。
  3. \(f[]\) 需要初始化。 是第一個問題的延伸,因為前面 \(m\) 個點沒有初始值。暴力更新即可。

程式碼

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const ll T=9e6+10,inf=1e18;
ll n,m,mxt,cnt[T],sum[T],f[T],q[T],h,t,ans=inf;

lb Y( int num ) { return f[num]+sum[num]; }
lb X( int num ) { return cnt[num]; }
lb slope( int x,int y )
{
        if ( X(x)==X(y) ) return Y(y)>Y(x) ? inf*1.0 : inf*-1.0;
        return ( Y(y)-Y(x)) / ( X(y)-X(x) );
}

int main()
{
        scanf( "%lld%lld",&n,&m );
        for ( ll i=1,ti; i<=n; i++ )
        {
                scanf( "%lld",&ti ),mxt=max( ti,mxt );
                sum[ti]+=ti; cnt[ti]++;
        }
        for ( ll i=1; i<=mxt+m; i++ )
                sum[i]+=sum[i-1],cnt[i]+=cnt[i-1];

        h=t=1; q[t]=0;
        for ( int i=1; i<m; i++ ) f[i]=cnt[i]*i-sum[i];
        for ( int i=m; i<=mxt+m; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])<=1.0*i ) h++;
                f[i]=f[q[h]]+(cnt[i]-cnt[q[h]])*i-(sum[i]-sum[q[h]]);
                while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i-m+1) ) t--;
                q[++t]=i-m+1;
                if ( i>=mxt ) ans=min( ans,f[i] ); 
        }

        printf( "%lld",ans );
}

3——任務安排(套餐)

題目連結

LOJ10184
AcWing300
P2365
任務安排1

LOJ10185
AcWing301
任務安排2

LOJ10186
AcWing302
P5785
任務安排3

注:此處範圍、AC程式碼均以 AcWing 為準。

題意

\(N\) 個任務等待完成(順序不改變),這 \(N\) 個任務被分成若干批,每批包含相鄰的若干任務。從時刻 \(0\) 開始,這些任務被分批加工,第 \(i\) 個任務單獨完成所需的時間是 \(Ti\) 。只有一臺機器,在每批任務開始前,機器需要啟動時間 \(S\),完成這批任務所需的時間是各個任務需要時間的總和(同一批任務將在同一時刻完成)。每個任務的費用是它的完成時刻乘以它的費用係數 \(Ci\)。請確定一個分組方案,使得總費用最小。

T1

\(1⩽N⩽5000,0⩽S⩽50,1⩽T_i,C_i⩽100\)

\(sumT[i]=\sum_{j=1}^i T[j],sumC[i]=\sum_{j=1}^i F[j]\),那麼

\[f[p][i]=min( f[p-1][j]+(sumT[i]+p\times S)\times (sumC[i]-sumC[j]) ) \]

考慮費用提前計算。每次分出一批任務,對後面的每個任務的用時都會產生 \(S\) 的貢獻,那麼可以提前計算。

\[f[i]=min(f[j]+sumT[i]\times ( sumC[i]-sumC[j] )+S\times (sumC[n]-sumC[j]) ) \]

程式碼


#include <bits/stdc++.h>
using namespace std;
const int N=5010;
int n,s,t[N],c[N],f[N];

int main() 
{
	scanf( "%d%d",&n,&s );
	for ( int i=1; i<=n; i++ )
		scanf( "%d%d",t+i,c+i );
    
    memset( f,0x3f,sizeof f );
    f[0]=0;
    for ( int i=1; i<=n; i++ )
    	t[i]+=t[i-1],c[i]+=c[i-1];
    for ( int i=1; i<=n; i++ )
     for ( int j=0; j<i; j++ )
     	f[i]=min( f[i],f[j]+(c[i]-c[j])*t[i]+s*(c[n]-c[j]) );
    
    printf( "%d\n",f[n] );
} 

T2

\(1\leq N\leq 3e5\) \(1\leq T_i,C_i\leq 512,0\leq S\leq 512\)

\(N\) 變大了,需要加上斜率優化。

\[f[i]=min(f[j]+sumT[i]\times (sumC[i]-sumC[j])+S\times (sumC[n]-sumC[j]) ) \]

考慮轉化成斜率式子。

\[f[j]=(sumT[i]+S)\times sumC[j] + f[i]-sumC[i]\times sumT[i]-sumC[n]\times S \]

程式碼

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=3e5+10;
int n,s,q[N],h=0,t=0;
ll sc[N],st[N],f[N];

lb slope( int x,int y ) { return (lb)(f[y]-f[x])/(sc[y]-sc[x]); }

int main()
{
	scanf( "%d%d",&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];
	}
	
	for ( int i=1; i<=n; i++ )
	{
		while ( h<t && slope(q[h],q[h+1])<=( st[i]+s ) ) h++;
		f[i]=f[q[h]]-( st[i]+s )*sc[q[h]]+sc[i]*st[i]+s*sc[n];
		while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i) ) t--;
		q[++t]=i;
	}
	
	printf( "%lld",f[n] );
}

T3

\(1\leq N\leq 3e5,0\leq S,C_i\leq 512,|T_i|\leq 512\)

任務的執行時間 \(t\) 可能是負數,那麼斜率不具有單調性,
就不能只保留大於 \(S+sumT[i]\) 的部分,而應該維護整個凸殼
此時隊頭不一定是最優決策,需要進行二分查詢,求出一個位置,
使左側的斜率小於 \(S+sumT[i]\) ,右側斜率大於 \(S+sumT[i]\)

注:此題 AcWing 上資料較強,建議把 slope 改為交叉相乘,需要使用 __int128 或者 doulbe .

程式碼

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=3e5+10;
int n,s,q[N],h=0,t=0;
ll sc[N],st[N];
double f[N];

int main()
{
	scanf( "%d%d",&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];
	}
	
	for ( int i=1; i<=n; i++ )
	{
		int l=h,r=t;
		while ( l<r )
		{
			int mid=(l+r)/2;
			if ( (f[q[mid+1]]-f[q[mid]])>(st[i]+s)*(sc[q[mid+1]]-sc[q[mid]]) ) r=mid;
			else l=mid+1;
		}
		f[i]=f[q[l]]-( st[i]+s )*sc[q[l]]+sc[i]*st[i]+s*sc[n];
		while ( h<t && ( f[q[t]]-f[q[t-1]] )*( sc[i]-sc[q[t]] )>=( f[i]-f[q[t]] )*( sc[q[t]]-sc[q[t-1]]  ) ) t--;
		q[++t]=i;
	}
	
	printf( "%.0lf",f[n] );
}

中場休息

例題完成了qwq。習題部分由於太多,一天之內無法整理完,所以是選做。(是隨便挑了幾道,不是挑了幾道好題emmm)

4——P2900 [USACO08MAR]Land Acquisition G

題目連結

題意

\(n\) 塊土地分組,每組的價格是這組土地中最大的長寬乘積,問買下所有土地的最小花費。

思路

沒想到隨手一點挑到了一道有意思的題。

如果你啥都不幹的話,dp方程:

\[f[i]=min( f[j]+calc(j+1,i)) \]

\(5e4\) 的 N,顯然 T飛了。但是你又覺得這個區間最大很難維護。怎麼辦呢?

考慮一塊土地到底該怎麼算。顯然,對於一塊地 \(x\) ,如果存在一個 \(y\) 的長寬均大於它,那麼這塊土地跟沒有一樣。所以可以把所有土地按長度排序,長度相同寬度排序。維護一個棧,將土地依次加入,每次加入的時候把所有寬度小於等於它的刪除即可。

此時留下的土地寬度降序。顯然在最優決策下,每組土地是連續的一段。

那麼終於可以得到正確的方程:

\[f[i]=min(f[i],f[j]+w[j+1]\times l[i]) \]

\(O(n^2)\) 還是過不去。看到 \(w[j+1]\times l[i]\) 考慮斜優。

\[f[j]=-w[j+1]\times l[i]+f[i] \]

程式碼

#include <bits/stdc++.h>
#define ll long long
#define lb long double 
using namespace std;
const int N=5e4+10;
struct land
{
	ll x,y;
	bool operator < ( const land tmp ) 
	{ if ( x!=tmp.x ) return x<tmp.x; return y<tmp.y; }
}a[N],sta[N];
int n,top=0,q[N],h,t;
ll f[N];

void init()
{
	sort( a+1,a+1+n ); top++; sta[top]=a[1];
	for ( int i=2; i<=n; i++ )
	{
		while ( top && sta[top].y<=a[i].y ) top--;
		top++; sta[top]=a[i];
	}
	n=top;
}

lb slope( int x,int y ) { return (lb)(f[y]-f[x])/(-sta[y+1].y+sta[x+1].y); }

int main()
{
	scanf( "%d",&n );
	for ( int i=1; i<=n; i++ )
		scanf( "%lld%lld",&a[i].y,&a[i].x );
	
	init();
	h=t=1; q[1]=0;
	for ( int i=1; i<=n; i++ )
	{
		while ( h<t && slope(q[h],q[h+1])<=1.0*sta[i].x ) h++;
		f[i]=f[q[h]]+sta[q[h]+1].y*sta[i].x;
		while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i) ) t--;
		t++; q[t]=i;
	}

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

5——P2120 [ZJOI2007]倉庫建設

題目連結: AcWing
luogu

題意

\(n\) 個工廠,由高到低分佈在一座山上,工廠 \(1\) 在山頂,工廠 \(n\) 在山腳。第 \(i\) 個工廠目前有成品 \(p_i\) 件,在第 \(i\) 個工廠位置建立倉庫的費用是 \(c_i\). 對於沒有建立倉庫的工廠,其產品被運往其他的倉庫,產品只能往山下運(只能運往編號更大的工廠的倉庫),一件產品運送一個單位距離的費用是 \(1\).假設建立的倉庫容量都足夠大。工廠 \(i\)\(1\) 的距離是 \(x_i\),問總費用最小值。

思路

\[f_i=min(f_j+x_i\times \sum_{l=j+1}^i(p_l)-\sum_{l=j+1}^i(x_l\times p_l) )+c_i,0\leq j<i \]

字首和,設 \(sp_i=\sum_{j=1}^i p_j,s_i=\sum_{j=1}^i p_j\times x_j\)
得到新的式子:

\[f_i=f_j+x_i\times (sp_i-sp_{j})-(s_i-s_j)+c_i \]

然後斜率優化:

\[f_j+s_j=x_i\times sp_j+(f_i+s_i-c_i-x_i\times sp_i) \]

程式碼

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=1e6+10;
int n,q[N],h,t;
ll x[N],p[N],c[N],f[N],sp[N],s[N];

ll X( int num ){ return sp[num]; }
ll Y( int num ){ return f[num]+s[num]; }
lb slope( int n1,int n2 ) { return (lb)(Y(n2)-Y(n1))/(X(n2)-X(n1)); }

int main()
{
        scanf( "%d",&n );
        for ( int i=1; i<=n; i++ )
                scanf( "%lld%lld%lld",&x[i],&p[i],&c[i] );
        sp[0]=s[0]=0;
        for ( int i=1; i<=n; i++ )
                sp[i]=sp[i-1]+p[i],s[i]=s[i-1]+p[i]*x[i];
        
        h=t=1; q[1]=0;
        for ( int i=1; i<=n; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])<=(lb)x[i] ) h++;
                f[i]=f[q[h]]+x[i]*(sp[i]-sp[q[h]])-(s[i]-s[q[h]])+c[i];
                while ( h<t && slope(q[t-1],q[t])>=slope(q[t],i) ) t--;
                t++; q[t]=i;
        }

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

6——P3628 [APIO2010]特別行動隊

題目連結:AcWing luogu

題意

有一支由 \(n\) 名士兵組成的部隊,士兵從 \(1\)\(n\) 編號,要將他們拆分成若干個特別行動隊調入戰場。同一支行動隊的隊員的編號應該連續。

編號為 \(i\) 的士兵的初始戰鬥力為 \(x_i\) ,一支隊伍的初始戰鬥力為所有隊員初始戰鬥力之和,記為 \(x\) 。最終戰鬥力為:\(x'=ax^2+bx+c.(a<0)\).

試求出最終戰鬥力之和的最大值。

思路

\(s[i]=\sum_{j=1}^i x_j.\)

\[f[i]=f[j]+a\times (s[i]-s[j])^2+b\times (s[i]-s[j])+c. \\\\ =f[j]+as[i]^2-2as[i]s[j]+as[j]^2+bs[i]-bs[j]+c \\\\ =(f[j]+as[j]^2-bs[j])+(as[i]^2+bs[i]+c)-2as[i]s[j] \]

所以整合得到:

\[f[j]+as[j]^2-bs[j]=2as[i]s[j]+(f[i]-as[i]^2-bs[i]-c) \]

程式碼

#include <bits/stdc++.h>
#define ll long long
#define lb long double
using namespace std;
const int N=1e6+10;
int n,q[N],h,t;
ll s[N],a,b,c,f[N];

ll X( int num ){ return s[num]; }
ll Y( int num ){ return f[num]+a*s[num]*s[num]-b*s[num]; }
lb slope( int n1,int n2 ) { return (lb)(Y(n2)-Y(n1))/(X(n2)-X(n1)); }

int main()
{
        scanf( "%d",&n ); scanf( "%lld%lld%lld",&a,&b,&c );
        s[0]=0;
        for ( int i=1; i<=n; i++ )
                scanf( "%lld",&s[i] ),s[i]+=s[i-1];
        
        h=t=1; q[1]=0;
        for ( int i=1; i<=n; i++ )
        {
                while ( h<t && slope(q[h],q[h+1])>=(lb)2.0*a*s[i] ) h++;
                int j=q[h]; f[i]=f[j]+a*s[j]*s[j]-b*s[j]+a*s[i]*s[i]+b*s[i]+c-2*a*s[i]*s[j];
                while ( h<t && slope(q[t-1],q[t])<=slope(q[t],i) ) t--;
                t++; q[t]=i;
        }

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

7——P4027 [NOI2007]貨幣兌換

題目連結
luogu
AcWing

又是 AcWing 眾多惡評題之一(

題意

太長了,自己看題

又是用NOI題作結的一天呢

本題沒有部分分,你的程式的輸出只有和標準答案相差不超過 \(0.0010.001\) 時,才能獲得該測試點的滿分,否則不得分。

輸入檔案可能很大,請採用快速的讀入方式。

必然存在一種最優的買賣方案滿足:

每次買進操作使用完所有的人民幣,每次賣出操作賣出所有的金券。

思路

這道題就是典型的不能單調棧,要平衡樹或者 CDQ 的題目 就算是早年NOI也沒那麼簡單呢qaq

\(f[i]\) 表示到第 \(i\) 天最多有多少錢,\(g[i]\) 表示用第 \(i\) 天的錢最多能買多少 \(B\) 券。易知 \(g[i]=\dfrac{f[i]}{r[i]\times a[i]+b[i]}\)

得到轉移:

\[f[i]=max( max_{j=1}^{i-1}( g[j]\times \dfrac{b[i]}{a[i]}+r[j]\times g[j] ) \times a[i],f[i-1]) \]

外面的 \(max\) 單獨判斷即可。主要是斜優裡面那個式子。

\[x=\dfrac{b[i]}{a[i]},k=g[j],b=r[j]\times g[j] \]

但是你發現斜率不單調。所以根據前文所述,要使用平衡樹或者CDQ分治維護了。

由於平衡樹 太長 太板子了,這裡就不作介紹,採用 CDQ分治維護。

CDQ分治比較好的總結可以看這裡,順便我的LCT也是這個博主教的

現在 假裝你已經會CDQ分治了 考慮分治,對於任意一個 \(f[i]\) ,只需要考慮 \(1\leq j\leq i-1\) 即可,和CDQ的解決方式非常相像。

對於一段區間 \([l,r]\) ,先遞迴左子區間 \([l,m]\) 保證 \([l,m]\)\(f,g\) 都已經得到;把左子區間按 \(k\) 遞增排序,就可以按斜率優化 \(O(n)\) 轉移;再把右子區間按在原序列中的位置遞增排序,遞迴。此時左子區間對右子區間的影響已經考慮完畢。注意邊界 \(l==r\) 。複雜度是 \(O(nlog^2n).\)

繼續優化。考慮 CDQ本身就是類似歸併排序的過程,可以用這個去掉排序複雜度。

我們希望得到什麼呢?當我們拿到 \([l,r]\) 這個區間的時候,\(x\) 是單調的。於是把外面的原序列按 \(x\) 遞增排序;拿到這個序列之後,我們希望在原序列中靠左的東西去左子區間,把 \([l,r]\) 掃一遍,把在原序列中 \(\leq m\) 的東西放左邊,\(\ge m\) 的東西放右邊,而且左右區間對 \(x\) 的單調性沒有影響。在遞迴右子區間的時候,希望左子區間回來的時候關於 \(k\) 單調遞增,所以最後對 \(k\) 做一遍歸併。複雜度 \(O(nlogn).\)

程式碼

寫了將近半個晚上啊啊啊啊啊

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const double eps=1e-8;
int q[N],n;
double f[N], g[N];
struct node
{
        int id; double a,b,r,x;
        node() {}
        node( int id,double a,double b,double r ) : id(id),a(a),b(b),r(r),x(b/a) {}
        bool operator < (node tmp ) { return x!=tmp.x ? x<tmp.x : id<tmp.id; }
}p[N],b[N];

double cross( int u,int v ) 
{
        return (p[u].r*g[p[u].id]-p[v].r*g[p[v].id])/(g[p[v].id]-g[p[u].id]);
}

double calc( int u,int v ) { return g[p[u].id]*(p[v].x+p[u].r); }

void update( int u,double v )
{
        if ( f[p[u].id]<v ) f[p[u].id]=v,g[p[u].id]=f[p[u].id]/(p[u].b+p[u].r*p[u].a);
}

void solve( int l,int r )
{
        if ( l==r ) { update(l,f[p[l].id-1]); return; }
        int m=(l+r)>>1,i,h,t;
        for ( h=l,t=m+1,i=l; i<=r; i++ )
                p[i].id<=m ? b[h++]=p[i] : b[t++]=p[i];
        for ( int i=l; i<=r; i++ )
                p[i]=b[i];
        solve( l,m ); h=1; t=0;
        for ( i=l; i<=m; i++ )
        {
                while ( h<t && cross(q[t],i)<cross(q[t-1],i)+eps ) t--;
                q[++t]=i;
        }
        for ( ; i<=r; i++ )
        {
                while ( h<t && calc( q[h],i )<calc( q[h+1],i )+eps ) h++;
                update( i,calc(q[h],i)*p[i].a );
        }
        solve( m+1,r );
        for ( h=l,t=m+1,i=l; h<=m && t<=r; )
                g[p[h].id]<g[p[t].id] ? b[i++]=p[h++] : b[i++]=p[t++];
        while ( h<=m ) b[i++]=p[h++];
        while ( t<=r ) b[i++]=p[t++];
        for ( i=l; i<=r; i++ ) p[i]=b[i];
}

int main()
{
        scanf( "%d%lf",&n,&f[1] );
        double a,b,r;
        for ( int i=1; i<=n; i++ )
                scanf( "%lf%lf%lf",&a,&b,&r ),p[i]=node(i,a,b,r);
        
        g[1]=f[1]/(p[1].r*p[1].a+p[1].b); sort( p+1,p+1+n );
        solve( 1,n );

        printf( "%.3lf\n",f[n] );
}

8——注意事項

  • 寫出DP方程之後要判斷能不能用斜優 不要無腦使用啊 ,即是否存在 \(g(i)\times g(j)\) 或者 \(\dfrac{Y(j)-Y(j')}{X(j)-X(j')}\) 的形式。
  • 通過大於小於符號或者 \(b\) 中的 \(f[i]\) 結合要求 \(min/max\) 判斷上下凸包,不要盲猜
  • \(X(j)\) 非嚴格遞增時,在求斜率時可能會出現相等的情況,這時候一定要特判(除數不能為0),return Y(j)>=Y(i) ? inf : -inf ,不要直接返回。
  • 比較 \(k_0[i],slope(j_1,j_2)\) 要寫規範,可能出現同除負數卻沒有變號。
  • 佇列初始化要塞一個點 \(P(0),j=0\)
  • 出入佇列的判斷要至少兩個元素(不然沒有斜率),所以不能寫 h<=t ,應該寫 h<t .
  • 計算斜率的除法有誤差,用 long double
  • 有可能出現部分dp初始值無法轉移,需要預處理(如擺渡車)
  • 比較兩個斜率的時候儘量寫 <=,>= 以避免有重點時分母出問題。但保險起見建議加上第三條中的判斷。

Last——鳴謝

和上一篇一樣,是對 @[Xing_Ling]
這篇博文
的學習整理。同樣適合作為初學教材。