#Part1-二分棧優化DPios
二分棧主要用來優化知足決策單調性的DP轉移式。 即咱們設$P[i]$爲$i$的決策點位置,那麼$P[i]$知足單調遞增的性質的DP。數組
因爲在這種DP中,知足決策點單調遞增,那麼對於一個點來講,以它爲決策點的點必定是一段連續的區間。函數
因此咱們能夠枚舉以哪一個點做爲決策點,去找到它所對應的以它爲決策點的區間。 考慮如何找到一個點的區間:優化
能夠發現,在當前狀況下(枚舉到以某個點做爲決策點的狀況下),該點所對應的區間必定爲[L,N].(L可能等於N+1)spa
那麼咱們能夠用一個棧來存儲區間[L,N]中的L,每次新枚舉到一個決策點$i$,就用棧頂L判斷,看L是用原決策點更優,仍是用新決策點$i$更優。 由於知足決策單調性,因此若用新決策點更優的話,該L就沒有意義了,就直接能夠從棧頂彈出。 咱們一直執行以上操做,直到遇到一個L的原決策點比新決策點$i$更優,那麼說明這個L仍是有意義的,因此不能彈。 而後咱們就須要去二分一個點出來做爲新的L,使得這個點右邊的點以$i$爲決策點更優,左邊的點以$i$爲決策點更劣。 以上就是二分棧的基本思路。.net
舉個例子: 決策點:<font color=blue>1111111111</font> 棧:1(1) 決策點:<font color=blue>111</font><font color=red>2222222</font> 棧:1(1) 4(2) 決策點:<font color=blue>111</font><font color=red>22222</font><font color=green>33</font> 棧:1(1) 4(2) 9(3) 決策點:<font color=blue>111</font><font color=red>222</font><font color=orange>4444</font> 棧:1(1) 4(2) 7(4) 注:棧裏應該有兩個信息,一個是L,一個是轉移點. (咱們不能維護每一個點的轉移點,那樣會提升時間複雜度)3d
代碼實現思路: ①定義一個隊首指針,對於目前枚舉到的決策點$i$,若$i$未被隊首指針的區間包含,那麼指針前移,直到$i$被包含,而後更新$i$的DP值。($i$的決策點就是目前隊首指針所對應的轉移點) ②判斷目前棧頂的L以$i$爲決策點更優,仍是以原決策點更優。若以$i$更優,彈出棧頂,而後,循環往復②操做。 ③對於目前的棧,判斷一下,棧是否爲空:指針
對於大多關於二分棧的題,通常是發現有單調性後就直接套版了。 因此在使用二分棧時,通常須要先證實DP的決策單調性(通常使用打表法證實),限制仍是很大。 注:有轉移限制的DP對二分棧限制很大,只有在限制也知足單調性的狀況下才能用。 (好比CSP2019D2T2劃分就能夠用類二分棧作法過掉$O(N*log(N))$能過的全部點)code
#include<cstdio> #include<algorithm> using namespace std; const long long ONE=1; const int MOD=(1<<30); const int MAXM=100005; const int MAXN=40000005; const long long INF=4e18; int N,TYP,Pt[MAXN]; long long A[MAXN],Dp[MAXN]; int Stac[MAXN],ID[MAXN],L,R; void Prepare(){ scanf("%d%d",&N,&TYP); if(TYP==1){ int X,Y,Z,M; int P[MAXM]={0},B[MAXN]={0}; scanf("%d%d%d%d%d%d",&X,&Y,&Z,&B[1],&B[2],&M); for(int i=3;i<=N;i++)B[i]=(ONE*B[i-1]*X+ONE*B[i-2]*Y+Z)%MOD; for(int i=1,L,R;i<=M;i++){ scanf("%d%d%d",&P[i],&L,&R); for(int j=P[i-1]+1;j<=P[i];j++) A[j]=B[j]%(R-L+1)+L; } return ; } for(int i=1;i<=N;i++) scanf("%lld",&A[i]); } int main(){ Prepare(); for(int i=1;i<=N;i++) A[i]=A[i-1]+A[i]; for(int i=1;i<=N;i++){ while(Stac[L+1]<=i&&L<R)L++; long long x=A[i]-A[ID[L]]; Dp[i]=Dp[ID[L]]+x*x;Pt[i]=ID[i]; int l=i,r=N+1; while(L<=R&&A[Stac[R]]-A[i]>=x)R--; if(L>R){Stac[++R]=i+1;ID[R]=i;continue;} while(l+1<r){ int mid=(l+r)/2; if(x<=A[mid]-A[i])r=mid; else l=mid; } if(r==N+1)continue; Stac[++R]=r;ID[R]=i; } printf("%lld\n",Dp[N]); }
其實主要是證單調性,其它的部分都比較版。blog
(雖然說這是個斜率優化板題呢...) 最終核心大意:給出了$P$數組與一個常數$L$,其中$P$數組知足單調遞增的性質。 有一個Dp轉移式:$Dp[i]=min{Dp[j]+(P[i]-P[j]-L)^2};$ 單調性證實以下: 採用反證:設有$A,B,C,D(A<B<C<D)$,其中$A$爲$D$的最優決策點,$B$爲$C$的最優決策點。(即要證實這種狀況不存在) 那麼有$$Dp[A]+(P[D]-P[A]-L)^2\le Dp[B]+(P[D]-P[B]-L)^2$$ $$Dp[B]+(P[C]-P[B]-L)^2\le Dp[A]+(P[C]-P[A]-L)^2$$ 能夠獲得: $$(P[D]-P[A]-L)^2+(P[C]-P[B]-L)^2\le (P[D]-P[B]-L)^2+(P[C]-P[A]-L)^2$$ 化簡得: $$2*(P[B]-P[A])*(P[D]-P[C])\le0$$ 與條件不符,故不存在這種狀況,即證實該Dp有決策單調性。
#include<cstdio> #include<string> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int MAXN=50005; int N,Len,A[MAXN],Pt[MAXN]; long long S[MAXN],Dp[MAXN]; int Stac[MAXN],ID[MAXN],L,R; long long W(int i,int j){ return (S[i]-S[j]-Len)*(S[i]-S[j]-Len); } int main(){ scanf("%d%d",&N,&Len);Len++; for(int i=1;i<=N;i++) scanf("%d",&A[i]),S[i]=S[i-1]+A[i]; for(int i=1;i<=N;i++)S[i]+=i; for(int i=1;i<=N;i++){ while(Stac[L+1]<=i&&L<R)L++; Dp[i]=Dp[ID[L]]+W(i,ID[L]); while(L<=R&&Dp[ID[R]]+W(Stac[R],ID[R])>=Dp[i]+W(Stac[R],i))R--; if(R<L)Stac[++R]=i+1,ID[R]=i; else{ int l=i,r=N+1; while(l+1<r){ int mid=(l+r)/2; if(Dp[ID[R]]+W(mid,ID[R])>=Dp[i]+W(mid,i))r=mid; else l=mid; } if(r==N+1)continue; Stac[++R]=r;ID[R]=i; } } printf("%lld\n",Dp[N]); return 0; } /* Dp[i]=Min{Dp[j]+W(i,j)}; */
最終核心大意:給出了$P$數組與一個常數$L$及一個參數$K$,其中$P$數組知足單調遞增的性質。 有一個Dp轉移式:$Dp[i]=min{Dp[j]+|P[i]-P[j]-L|^K};$ 單調性證實以下:(沿用T1的思路) 採用反證:設有$A,B,C,D(A<B<C<D)$,其中$A$爲$D$的最優決策點,$B$爲$C$的最優決策點。(即要證實這種狀況不存在) 那麼有$$Dp[A]+|P[D]-P[A]-L|^K\le Dp[B]+|P[D]-P[B]-L|^K$$ $$Dp[B]+|P[C]-P[B]-L|^K\le Dp[A]+|P[C]-P[A]-L|^K$$ 能夠獲得: $$|P[D]-P[A]-L|^K+|P[C]-P[B]-L|^K\le |P[D]-P[B]-L|^K+|P[C]-P[A]-L|^K$$ 而後...... 咱們設$X=P[B]-P[A],Y=P[C]-P[B],Z=P[D]-P[C];$ 那麼有:$$|X+Y+Z-L|^K+|Y-L|^K\le |Y+Z-L|^K+|X+Y-L|^K$$ 咱們不妨畫出$F(t)=|t-L|^K$的圖像,就像這樣: 而後在圖像上將那四個點標出來。 發現$(X+Y+Z-L)+(Y-L)=(Y+Z-L)+(X+Y-L)$,即這四個點的橫座標是關於$E=\frac{X+2*Y+Z}{2}$對稱的。 但因爲那四個點的分佈狀況繁多,因此不妨分類討論(因爲左邊右邊本質是同樣的,因此這裏只討論一邊的狀況): ①:左二右二(左邊兩個點,右邊兩個點) 這種狀況下,顯然$F(Y)+F(X+Y+Z)\ge F(X+Y)+F(Y+Z)$ 故與條件不符。 ②:左一右三(左邊一個點,右邊三個點) 那麼這種狀況下,咱們將$Y$翻轉至$Y$,那麼此時有$DX1<DX2,DY1<DY2$,即$$F(Y+Z)-F(Y)=F(Y+Z)-F(Y
)<F(X+Y+Z)-F(X+Y)$$ 即有$$F(Y+Z)+F(X+Y)<F(X+Y+Z)+F(Y)$$ 故與條件不符。 ③:左零右四(左邊零個點,右邊四個點)
這種狀況下有$DX1=DX2$,由函數斜率遞增的性質可得$DY1<DY2$ 故同②的狀況,與條件不符。
綜上,不存在給出狀況,故該Dp式知足決策單調性。 (證完單調性後就和玩具裝箱同樣了,故這裏就不給代碼了 )
其實也沒啥好引入的
約束:通常在使用分治優化的時候,DP是知足決策單調性的。 對於形同$$Dp1[i]=max/min{Dp2[j]+W(i,j)};$$這樣的DP式子,咱們通常是在$O(N^2)$出解。(即枚舉一個$i$,一個$j$)
可是因爲知足決策單調性,咱們能夠這樣想: 對於$Dp1$來講,咱們設待轉移區間($i$)即未更新區間爲$[L,R]$, 設目前可從$Dp2$轉移過來的點構成的區間($j$)即決策點區間爲$[A,B]$.
對於普通的轉移,咱們第一步會枚舉一個$Dp1[i]$出來進行轉移, 可是如今,咱們可使$i$變爲當前需轉移區間$[L,R]$的中心點$Mid=\frac{L+R}{2}$, 即每次轉移只轉移$Dp1[Mid]$,並順便找出$Dp1[Mid]$的決策點$P[Mid]$.
以後,咱們能夠把待轉移區間$[L,R]$分爲兩半:$[L,Mid-1]$和$[Mid+1,R]$. 而又因爲,咱們的$DP$是知足決策單調性的,因此決策點區間也能夠分紅兩半:$[A,P[Mid]]$與$[P[Mid],B]$. 而後就能夠遞推下去了。
又因爲咱們的DP是知足決策單調性的,因此正確性能夠保證。
而在每一層內,決策點總共被枚舉次數是$O(N)$的,一共有$log(N)$層。 故總的時間複雜度是$O(N*log(N))$.
主要仍是證單調性。
題意,有$N$我的,每兩我的$i,j$之間有$A[i][j]$的怨氣值。 定義一個組的怨氣和爲該組內任意兩我的的怨氣值之和。 現要求將這$N$我的分紅$K$組,使得這$K$組的怨氣和最小。 問最小怨氣和。
好吧,最終DP式子就是: $$DP[k][i]=min{DP[k-1][j-1]+\sum_{p=j}^i\sum_{q=p+1}^iA[p][q]};$$
單調性的話,證實其實比較簡單,這裏就不贅述了。
#include<cstdio> #include<algorithm> using namespace std; const int MAXN=4005; const int INF=0X3F3F3F3F; int N,K,A[MAXN][MAXN]; int Dp[MAXN][MAXN]; int Pt[MAXN][MAXN]; inline int Read(){ register int x=0; char c=getchar();bool f=0; while(c<'0'||c>'9'){if(c=='-')f^=1;c=getchar();} while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c-'0');c=getchar();} if(f==1)x=-x;return x; } int W(int i,int j){ return A[i][i]-A[i][j-1]-A[j-1][i]+A[j-1][j-1]; } void Solve(int k,int l,int r,int pl,int pr){ if(l>r)return ; int mid=(l+r)/2,pt=pl; Dp[k][mid]=INF; for(int i=pl;i<=min(mid,pr);i++){ int cost=Dp[k-1][i-1]+W(mid,i); if(cost<Dp[k][mid])Dp[k][mid]=cost,pt=i; } Solve(k,l,mid-1,pl,pt); Solve(k,mid+1,r,pt,pr); } int main(){ N=Read();K=Read(); for(int i=1;i<=N;i++) for(int j=1;j<=N;j++){ A[i][j]=Read(); A[i][j]+=A[i][j-1]+A[i-1][j]-A[i-1][j-1]; } for(int i=1;i<=N;i++)Dp[0][i]=INF; for(int k=1;k<=K;k++)Solve(k,1,N,1,N); printf("%d\n",Dp[K][N]/2); }
題意,給出$N$個數,現讓你將這$N$個數劃分爲$K$段, 定義某一段的代價爲該段內不一樣元素的個數,求最大總代價。
經過以上描述,易得最終DP式爲: $$DP[k][i]=max{DP[k-1][j-1]+W(i,j)};$$
其中$W(i,j)$表示$[j,i]$中不一樣的數的個數。
關於這個DP式的單調性,咱們能夠這樣想: 設有$A,B,C,D(A<B<C<D)$四個數, 其中$A$爲$D$的最優決策點,$B$爲$C$的最優決策點。
那麼相應的,就有 $$DP[k-1][B-1]+W(C,B)>DP[k-1][A-1]+W(C,A)$$ $$DP[k-1][A-1]+W(D,A)>DP[k-1][B-1]+W(D,B)$$ 即有: $$W(D,A)+W(C,B)>W(C,A)+W(D,B)$$ 咱們能夠這樣想,將$[A,D]$這個區間分紅以下幾個部分: 其中$X2$表示$W(C,B)$的值, 而$X1,X3$分別表示$[A,B],[C,D]$中與$[B,C]$間不一樣的數。 即:$X1+X2=W(C,A),X3+X2=W(D,B)$
那麼$$W(D,A)+W(C,B)>W(C,A)+W(D,B)$$ 這個式子就能夠寫做: $$W(D,A)>X1+X2+X3$$ 而上式顯然不成立,故該DP知足決策單調性。
討論了DP的決策單調性,那麼是否能夠直接套用以前的板呢?
然而不行,發如今如下板塊時:
void Solve(int k,int l,int r,int pl,int pr){ if(l>r)return ; int mid=(l+r)/2,pt=pl; Dp[k][mid]=-INF; for(int i=pl;i<=min(mid,pr);i++){ int cost=Dp[k-1][i-1]+W(mid,i); if(cost>=Dp[k][mid])Dp[k][mid]=cost,pt=i; } Solve(k,l,mid-1,pl,pt); Solve(k,mid+1,r,pt,pr); }
咱們算$W(mid,i)$沒法$O(1)$出解,同時有一個處理思路就是:
void Solve(int k,int l,int r,int pl,int pr){ if(l>r)return ; int mid=(l+r)/2,pt=pl; Dp[k][mid]=-INF; for(int i=mid;i>pr;i--)Tur(i); for(int i=min(mid,pr);i>=pl;i--){ Tur(i);int cost=Dp[k-1][i-1]+Cnt; if(cost>=Dp[k][mid])Dp[k][mid]=cost,pt=i; } Solve(k,l,mid-1,pl,pt); Solve(k,mid+1,r,pt,pr); }
其中,Tur(i)表示更新某一元素入答案中。 可是這樣作會多增長$[pr+1,mid]$的循環,從而增長時間複雜度。 從而被惡意出題人卡成TLE...
針對於以上狀況,咱們可使用一種相似於滑動的思想。 即便用兩個指針$L,R$,而後維護區間$W(L,R)$的值。
每次要求某個$W(l,r)$的時候,就將$L$滑動到$l$,$R$滑動到$r$,滑動途中維護$W(L,R)$就好了。
void Tur(int x,int k){ CCnt[Val[x]]+=k; if(CCnt[Val[x]]==0&&k==-1)Cnt--; if(CCnt[Val[x]]==1&&k==1)Cnt++; } long long W(int r,int l){ while(L>l)Tur(--L,1); while(R<r)Tur(++R,1); while(L<l)Tur(L++,-1); while(R>r)Tur(R--,-1); return Cnt; } void Solve(int k,int l,int r,int pl,int pr){ if(l>r)return ; int mid=(l+r)/2,pt=pl; Dp[k][mid]=-INF; for(int i=min(mid,pr);i>=pl;i--){ long long cost=Dp[k-1][i-1]+W(mid,i); if(cost>=Dp[k][mid])Dp[k][mid]=cost,pt=i; } Solve(k,l,mid-1,pl,pt); Solve(k,mid+1,r,pt,pr); }
而這樣的時間複雜度也是$O(N*log(N))$的。 緣由以下:
首先,因爲咱們函數的遞推結構是先左再右, 因此咱們的$L$指針移動的總步數是$O(N)$範圍的。 同時,咱們每次走的區間都是連續的,而對於任意一個位置,咱們最多隻會通過$O(log(N))$次。
因此,時間複雜度仍是$O(N*log(N))$的。
#include<cstdio> #include<algorithm> using namespace std; const int MAXK=55; const int MAXN=35005; const long long INF=1e18; int T,N,K,L,R,Ans; int Val[MAXN],Cnt,CCnt[MAXN]; long long Dp[MAXK][MAXN]; inline int Read(){ register int x=0; char c=getchar();bool f=0; while(c<'0'||c>'9'){if(c=='-')f^=1;c=getchar();} while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c-'0');c=getchar();} if(f==1)x=-x;return x; } void Tur(int x,int k){ CCnt[Val[x]]+=k; if(CCnt[Val[x]]==0&&k==-1)Cnt--; if(CCnt[Val[x]]==1&&k==1)Cnt++; } long long W(int r,int l){ while(L>l)Tur(--L,1); while(R<r)Tur(++R,1); while(L<l)Tur(L++,-1); while(R>r)Tur(R--,-1); return Cnt; } void Solve(int k,int l,int r,int pl,int pr){ if(l>r)return ; int mid=(l+r)/2,pt=pl; Dp[k][mid]=-INF; for(int i=min(mid,pr);i>=pl;i--){ long long cost=Dp[k-1][i-1]+W(mid,i); if(cost>=Dp[k][mid])Dp[k][mid]=cost,pt=i; } Solve(k,l,mid-1,pl,pt); Solve(k,mid+1,r,pt,pr); } int main(){ N=Read();K=Read(); for(int i=1;i<=N;i++)Val[i]=Read(); for(int i=1;i<=N;i++)Dp[0][i]=-INF; for(int k=1;k<=K;k++)Solve(k,1,N,1,N); printf("%lld\n",Dp[K][N]); }
打表法好啊。。。