关于单调性优化DP算法的理解

#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$更优,弹出栈顶,而后,循环往复②操做。 ③对于目前的栈,判断一下,栈是否为空:指针

  • 若为空,直接让新的信息入栈。
  • 若不为空,二分新决策点L的位置(此处全部点的原决策点都是目前栈顶的原决策点),入栈。 (注:记得特判L!=N+1)

小结

对于大多关于二分栈的题,通常是发现有单调性后就直接套版了。 因此在使用二分栈时,通常须要先证实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

T1玩具装箱

(虽然说这是个斜率优化板题呢...) 最终核心大意:给出了$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)};
*/

T2诗人小G

最终核心大意:给出了$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式知足决策单调性。 (证完单调性后就和玩具装箱同样了,故这里就不给代码了 )

Part2-分治优化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))$.

例题

主要仍是证单调性。

T1Ciel and Gondolas

题意,有$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);
}

T2The Bakery

题意,给出$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]);
}

后记

打表法好啊。。。

相关文章
相关标签/搜索