决策单调性学习笔记

决策单调性,通过决策点的单调性进行处理,从而优化 dp 的时间复杂度

对于这种题目,可以分为几个过程来完成题目:

  1. 敲暴力

  2. 观察是否有其它的优化方式

  3. 分析最优决策点位置/打表

  4. 得到如何使用决策单调性优化

    石子合并

区间合并问题基本转移如下:

\[ f_{l,r}=\min f_{l,k}+f_{k+1,r}+w_{i,j} \]

这类问题通常是可以通过决策单调性优化的,四边形不等式和区间单调性是通用解法,但是我认为具体题目具体分析会更方便一点。

石子合并这道题目就是一个典型,就算分析不好分析,但是直接看也能看出来怎样优化

石子的合并过程左右要基本类似,所以决策点应该在两个区间之间

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[2005],pre[2005][2005],dp[2005][2005],w[2005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],w[i]=a[i]+w[i-1];
	for(int i=n+1;i<=n*2;i++)a[i]=a[i-n],w[i]=a[i]+w[i-1];
	memset(dp,0x3f,sizeof(dp));
	for(int i=1;i<=2*n;i++)dp[i][i]=0,pre[i][i]=i;
	int ans=1e18;
	for(int len=2;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			for(int k=pre[l][r-1];k<=pre[l+1][r];k++){
				if(dp[l][k]+dp[k+1][r]+w[r]-w[l-1]<dp[l][r]){
					dp[l][r]=dp[l][k]+dp[k+1][r]+w[r]-w[l-1];
					pre[l][r]=k;
				}
			}
			if(len==n)ans=min(ans,dp[l][r]);
		}
	}
	cout<<ans;
	return 0;
}

羊羊列队

单调队列式斜率优化应该也是决策单调性的一种,但是专门讲过了。

能用斜率优化的一般都用斜率优化,比较快

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[10005],p[10005],head,tail,x[10005],y[10005],dp[10005];
bool slope(int i,int j,int k){
	return (y[i]-y[j])*(x[j]-x[k])<=(y[j]-y[k])*(x[i]-x[j]);
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	sort(a+1,a+n+1);
	memset(dp,0x3f,sizeof(dp));
	dp[0]=0;
	for(int i=1;i<=m;i++){
		for(int j=0;j<=n;j++)x[j]=2*a[j+1],y[j]=a[j+1]*a[j+1]+dp[j];
		head=1,tail=0;
		p[++tail]=i-1;
		for(int j=i;j<=n;j++){
			while(head<tail&&y[p[head+1]]-y[p[head]]<=a[j]*(x[p[head+1]]-x[p[head]]))head++;
			dp[j]=y[p[head]]-a[j]*x[p[head]]+a[j]*a[j];
			while(head<tail&&slope(j,p[tail],p[tail-1]))tail--;
			p[++tail]=j;
		}
	}
	cout<<dp[n];
	return 0;
}

Post加强版

需要一个精妙的状态,表示 \(i\) 为最后一个邮局时前面的所有花费

然后两点之间的费用就能用二分求出来了

为什么会有决策单调性?

因为我们从左往右处理,那么我们不可能会把前面的点的决策点之前的点拿过来,这比起前面点的决策点增加的费用还大,不可能作为新的决策点

顺便讲一讲如何用单调队列来处理答案

首先使用单调队列,前提是必须要最佳决策点不断往右靠,我们加入一个值时,要对比一下两个不同的点之间在哪个点时优劣发生改变,让队列从头到尾越前面越在早的时候优,如果已经不是优的区间了再出队

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,head,tail,p[100005],dp[100005],d[100005],c[100005],w[100005],pre[100005],sum1[100005],sum2[100005],k[100005];
int query(int x,int y){
	if(!x)return sum2[y]-pre[y]*(d[n]-d[y]);
	int l=x,r=y;
	while(l<=r){
		int mid=l+r>>1;
		if(d[y]-d[mid]>=d[mid]-d[x])l=mid+1;
		else r=mid-1;
	}
	int k=l-1;
	int res=sum1[k]-sum1[x-1]-(pre[k]-pre[x-1])*d[x]+sum2[y]-sum2[k]-(pre[y]-pre[k])*(d[n]-d[y]);
	return res;
}
int calc(int x,int y){
	return dp[x]+query(x,y)+c[y];
}
int ef(int x,int y){
	int l=max(x,y),r=n;
	while(l<=r){
		int mid=l+r>>1;
		if(calc(x,mid)>=calc(y,mid))r=mid-1;
		else l=mid+1;
	}
	return r;
}
void insert(int x){
	if(!tail)p[++tail]=x;
	else {
		while(head<tail&&k[tail-1]>=ef(p[tail],x))tail--;
		k[tail]=ef(p[tail],x);
		p[++tail]=x;
	}
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>d[i];
	for(int i=1;i<=n;i++)cin>>c[i];
	for(int i=1;i<=n;i++)cin>>w[i];
	for(int i=1;i<=n;i++)pre[i]=pre[i-1]+w[i];
	for(int i=1;i<=n;i++)sum1[i]=sum1[i-1]+w[i]*d[i];
	for(int i=1;i<=n;i++)sum2[i]=sum2[i-1]+w[i]*(d[n]-d[i]);
	head=1,tail=0;
	for(int i=1;i<=n;i++){
		insert(i-1);
		while(head<tail&&k[head]<i)head++;
		dp[i]=calc(p[head],i);
	}
	int ans=1e18;
	for(int i=1;i<=n;i++)ans=min(ans,dp[i]+(sum1[n]-sum1[i-1])-d[i]*(pre[n]-pre[i-1]));
	cout<<ans;
	return 0;
}

Lawrence

首先进行一点点简单的数学转化,然后把状态中的两维中的一维从小到大枚举,另外一维就通过分治来处理,分治还是很好理解且好写的,但是要求前面转移的那几维已经知道了才能用

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[1005],pre[1005],sum[1005],dp[1005][1005];
void solve(int p,int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1;
	int res=1e18,w;
	for(int i=L;i<=R;i++){
		int tmp=dp[i-1][mid-1]+(pre[p]-pre[i-1])*(pre[p]-pre[i-1])-(sum[p]-sum[i-1]);
		if(tmp<res)res=tmp,w=i;
	}
	dp[p][mid]=res;
	solve(p,l,mid-1,L,w);
	solve(p,mid+1,r,w,R);
}
signed main(){
	while(cin>>n>>m){
		if(!n&&!m)break;
		m++;
		for(int i=1;i<=n;i++)cin>>a[i];
		for(int i=1;i<=n;i++)pre[i]=pre[i-1]+a[i];
		for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i]*a[i];
		memset(dp,0x3f,sizeof(dp));
		dp[0][0]=0;
		for(int i=1;i<=n;i++)solve(i,1,min(i,m),1,i);
		cout<<dp[n][m]/2<<endl;
	}
	return 0;
}

征途

类似的题目,我仍然拿分治处理,当然这题也可以斜率优化

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,a[3005],pre[3005],dp[3005][3005];
void solve(int p,int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1;
	int res=1e18,w;
	for(int i=L;i<=R;i++){
		int tmp=dp[mid-1][i-1]+(pre[p]-pre[i-1])*(pre[p]-pre[i-1]);
		if(tmp<res)res=tmp,w=i;
	}
	dp[mid][p]=res;
	solve(p,l,mid-1,L,w);
	solve(p,mid+1,r,w,R);
}
signed main(){
	cin>>n>>m;
	int s=0;
	for(int i=1;i<=n;i++)cin>>a[i],s+=a[i];
	for(int i=1;i<=n;i++)pre[i]=pre[i-1]+a[i];
	memset(dp,0x3f,sizeof(dp));
	dp[0][0]=0;
	for(int i=1;i<=n;i++)solve(i,1,min(i,m),1,i);
	cout<<dp[m][n]*m-s*s;
	return 0;
}

Lightning Conductor

可以用分治,当然单调队列也行,虽然求的是较大值,但是由于根号是越小的里面只增大改变越大,所以决策点还是向右靠的

代码:

#include<bits/stdc++.h>
using namespace std;
int n,a[500005],dp[500005];
double sqr[500005];
double calc(int j,int i){
	return a[j]+sqr[i-j];
}
void solve(int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1;
	double res=0;
	int c;
	for(int i=L;i<=min(R,mid);i++){
		if(calc(i,mid)>res)res=calc(i,mid),c=i;
	}
	dp[mid]=max(dp[mid],(int)ceil(res));
	solve(l,mid-1,L,c);
	solve(mid+1,r,c,R);
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		sqr[i]=sqrt(i);
	}
	solve(1,n,1,n);
	reverse(a+1,a+n+1);
	reverse(dp+1,dp+n+1);
	solve(1,n,1,n);
	for(int i=n;i>=1;i--)printf("%d\n",dp[i]-a[i]);
	return 0;
}

柠檬

终于来了一道不一样的,这道题目需要用栈来实现,因为平方的底数增加相同的数,这个底数越大,增加的越猛。

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[100005],t[100005],sum[100005],dp[100005],op[100005];
vector<int> e[100005];
int calc(int x,int t){
	return dp[x-1]+a[x]*t*t;
}
int fe(int x,int y){
	int l=1,r=n,res=n+1;
	while(l<=r){
		int mid=l+r>>1;
		if(calc(x,mid-sum[x]+1)>=calc(y,mid-sum[y]+1))res=mid,r=mid-1;
		else l=mid+1;
	}
	return res;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],sum[i]=++t[a[i]];
	for(int i=1;i<=n;i++){
		int top=e[a[i]].size()-1;
		while(top>=1&&fe(e[a[i]][top-1],e[a[i]][top])<=fe(e[a[i]][top],i))top--,e[a[i]].pop_back();
		e[a[i]].push_back(i);
		top++;
		while(top>=1&&fe(e[a[i]][top-1],e[a[i]][top])<=sum[i])e[a[i]].pop_back(),top--;
		dp[i]=calc(e[a[i]][top],sum[i]-sum[e[a[i]][top]]+1);
	}
	cout<<dp[n];
	return 0;
}

Cow School牛学校

01分数规划后通过分治处理出一半最大另一半最小值即可

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,pret[50005],prep[50005],f[50005],g[50005];
struct P{
	int t,p;
}a[50005];
bool cmp(P a,P b){
	return a.t*b.p>b.t*a.p;
}
void solve1(int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1;
	f[mid]=1e16;
	int w;
	for(int i=L;i<=min(mid,R);i++){
		int tmp=a[i].t*prep[mid]-a[i].p*pret[mid];
		if(tmp<f[mid])f[mid]=tmp,w=i;
	}
	solve1(l,mid-1,L,w);
	solve1(mid+1,r,w,R);
}
void solve2(int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1;
	g[mid]=-1e16;
	int w;
	for(int i=max(L,mid+1);i<=R;i++){
		int tmp=a[i].t*prep[mid]-a[i].p*pret[mid];
		if(tmp>g[mid])g[mid]=tmp,w=i;
	}
	solve2(l,mid-1,L,w);
	solve2(mid+1,r,w,R);
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i].t>>a[i].p;
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++)pret[i]=pret[i-1]+a[i].t;
	for(int i=1;i<=n;i++)prep[i]=prep[i-1]+a[i].p;
	solve1(1,n-1,1,n);
	solve2(1,n-1,1,n);
	int ans=0;
	for(int i=1;i<n;i++)if(f[i]<g[i])ans++;
	cout<<ans<<endl;
	for(int i=n-1;i>=1;i--)if(f[i]<g[i])cout<<n-i<<endl;
	return 0;
}

最远点

把环拆成两个链,与自己环上的转移即可

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,n,x[1000005],y[1000005],dp[500005];
void solve(int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1;
	int dis=0,w=0;
	for(int i=max(L,mid);i<=min(mid+n,R);i++){
		int tmp=(x[i]-x[mid])*(x[i]-x[mid])+(y[i]-y[mid])*(y[i]-y[mid]);
		if(tmp>dis)dis=tmp,w=i;
	}
	dp[mid]=w;
	if(dp[mid]>n)dp[mid]-=n;
	solve(l,mid-1,L,w);
	solve(mid+1,r,w,R);
}
signed main(){
	cin>>T;
	while(T--){
		scanf("%lld",&n);
		for(int i=1;i<=n;i++)scanf("%lld%lld",&x[i],&y[i]),x[i+n]=x[i],y[i+n]=y[i];
		solve(1,n,1,2*n);
		for(int i=1;i<=n;i++)printf("%lld\n",dp[i]);
	}
	return 0;
}

记忆的轮廓

配合树上dp和期望dp而已

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,n,m,p;
double sum[1505],f[1505],c[1505][1505],dp[1505][1505];
vector<int> e[1505];
void dfs(int p){
	f[p]=1;
	for(int i:e[p]){
		dfs(i);
		f[p]+=f[i]/e[p].size();
	}
}
void solve(int p,int l,int r,int L,int R){
	if(l>r||L>R)return;
	int mid=l+r>>1,w=L;
	for(int i=L;i<=min(R,mid-1);i++){
		if(dp[p-1][w]+c[w][mid]>dp[p-1][i]+c[i][mid])w=i;
	}
	dp[p][mid]=dp[p-1][w]+c[w][mid];
	solve(p,l,mid-1,L,w);
	solve(p,mid+1,r,w,R);
}
signed main(){
	cin>>T;
	while(T--){
		cin>>n>>m>>p;
		p--;
		for(int i=1,x,y;i<=m-n;i++){
			cin>>x>>y;
			e[x].push_back(y);
		}
		for(int i=1;i<n;i++)e[i].push_back(i+1);
		for(int i=1;i<=n;i++){
			sum[i]=0;
			for(int j:e[i])if(j>n)dfs(j),sum[i]+=f[j];
		}
		for(int i=1;i<=n;i++){
			c[i][i]=0;
			for(int j=i+1;j<=n;j++)c[i][j]=c[i][j-1]*e[j-1].size()+e[j-1].size()+sum[j-1];
		}
		for(int i=2;i<=n;i++)dp[0][i]=1e18;
		dp[0][1]=0;
		for(int i=1;i<=p;i++)solve(i,1,n,1,n);
		printf("%.4lf\n",dp[p][n]);
		for(int i=1;i<=m;i++)e[i].clear();
	}
	return 0;
}

区间

抓住两个最优的性质开搞

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,ans;
struct P{
	int l,r;
}a[1000005];
bool cmp(P a,P b){
	if(a.l==b.l)return a.r>b.r;
	return a.l<b.l;
}
int calc(int i,int j){
	return (a[j].r-a[i].l)*max(a[i].r-a[j].l,0ll);
}
void solve(int l,int r,int L,int R){
	if(l>r)return;
	int mid=l+r>>1;
	int ma=0,w=0;
	for(int i=L;i<=min(R,mid-1);i++){
		int tmp=calc(i,mid);
		if(tmp>ma)ma=tmp,w=i;
	}
	ans=max(ans,ma);
	if(w){
		solve(l,mid-1,L,w);
		solve(mid+1,r,w,R);	
	}
	else {
		solve(l,mid-1,L,R);
		solve(mid+1,r,L,R);
	}
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)scanf("%lld%lld",&a[i].l,&a[i].r);
	sort(a+1,a+n+1,cmp);
	int cnt=1;
	for(int i=2;i<=n;i++){
		if(a[i].r>a[cnt].r)a[++cnt]=a[i];
		else ans=max(ans,(a[i].r-a[i].l)*(a[cnt].r-a[cnt].l));
	}
	solve(1,cnt,1,cnt);
	cout<<ans;
	return 0;
}

诗人小G

斜率优化里开的坑现在终于填上了,没什么特殊的

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int T,n,l,P,pre[100005],p[100005],nxt[100005],k[100005];
string s[100005];
long double dp[100005];
long double ksm(long double a,int b){
	long double s=1;
	while(b){
		if(b&1)s=s*a;
		a=a*a;
		b>>=1;
	}
	return s;
}
long double calc(int i,int j){
	return dp[j]+ksm(abs(pre[i]-pre[j]-l),P);
}
int ef(int x,int y){
	int l=x,r=n+1;
	while(l<r){
		int mid=l+r>>1;
		if(calc(mid,x)>=calc(mid,y))r=mid;
		else l=mid+1;
	}
	return r;
}
signed main(){
	cin>>T;
	while(T--){
		cin>>n>>l>>P;
		l++;
		for(int i=1;i<=n;i++)cin>>s[i];
		for(int i=1;i<=n;i++)pre[i]=pre[i-1]+s[i].size()+1;
		int head=1,tail=0;
		p[++tail]=0;
		for(int i=1;i<=n;i++){
			while(head<tail&&k[head]<=i)head++;
			dp[i]=calc(i,p[head]);
			nxt[i]=p[head];
			while(head<tail&&k[tail-1]>=ef(p[tail],i))tail--;
			k[tail]=ef(p[tail],i),p[++tail]=i;
		}
		if(dp[n]>1e18)puts("Too hard to arrange");
		else {
			printf("%.0Lf\n",dp[n]);
		}
	}
	return 0;
}

Mowing Mischief

转化为 dp 再考虑决策单调性,发现要关注从哪些区间转移,考虑线段树分治,再在里面分治即可

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,T,d[200005],cnt,f[200005],dp[200005];
struct P{
	int x,y;
}a[200005];
bool cmp(P a,P b){
	return a.x<b.x;
}
vector<int> e[200005];
bool check(int x,int y){
	return a[x].x<a[y].x&&a[x].y<a[y].y;
}
struct ST{
	vector<int> c[800005];
	int ls[800005],rs[800005],id;
	void init(){
		id=0;
	}
	int New(){
		id++;
		c[id].clear();
		ls[id]=rs[id]=0;
		return id;
	}
	void change(int &p,int l,int r,int L,int R,int x){
		if(!p)p=New();
		if(l>=L&&r<=R)return c[p].push_back(x);
		int mid=l+r>>1;
		if(mid>=L)change(ls[p],l,mid,L,R,x);
		if(mid<R)change(rs[p],mid+1,r,L,R,x);
	}
	void CDQ(int p,int w,int l,int r,int L,int R){
		if(l>r)return;
		int mid=l+r>>1;
		int k,mi=1e18;
		for(int i=L;i<=R;i++){
			int tmp=dp[e[w][i]]+(a[c[p][mid]].x-a[e[w][i]].x)*(a[c[p][mid]].y-a[e[w][i]].y);
			if(tmp<mi)mi=tmp,k=i;
		}
		dp[c[p][mid]]=min(mi,dp[c[p][mid]]);
		CDQ(p,w,l,mid-1,k,R);
		CDQ(p,w,mid+1,r,L,k);
	}
	void query(int p,int w,int l,int r){
		if(!p)return;
		CDQ(p,w,0,c[p].size()-1,l,r);
		int mid=l+r>>1;
		query(ls[p],w,l,mid),query(rs[p],w,mid+1,r);
	}
}seg;
int findl(int x,int id){
	int l=0,r=e[id].size()-1;
	while(l<=r){
		int mid=l+r>>1;
		if(a[e[id][mid]].y<a[x].y)r=mid-1;
		else l=mid+1;
	}
	return r+1;
}
int findr(int x,int id){
	int l=0,r=e[id].size()-1;
	while(l<=r){
		int mid=l+r>>1;
		if(a[e[id][mid]].x<a[x].x)l=mid+1;
		else r=mid-1;
	}
	return l-1;
}
signed main(){
	cin>>n>>T;
	for(int i=1;i<=n;i++)cin>>a[i].x>>a[i].y;
	sort(a+1,a+n+1,cmp);
	a[++n]={T,T},a[0]={0,0};
	for(int i=0,w;i<=n;i++){
		if(!cnt||a[i].y>d[cnt])w=++cnt;
		else w=lower_bound(d+1,d+cnt+1,a[i].y)-d;
		d[w]=a[i].y;
		e[w].push_back(i);
	}
	memset(dp,0x3f,sizeof(dp));
	dp[0]=0;
	for(int i=2;i<=cnt;i++){
		int rt=0;
		seg.init();
		for(int j:e[i])seg.change(rt,0,e[i-1].size()-1,findl(j,i-1),findr(j,i-1),j);
		seg.query(rt,i-1,0,e[i-1].size()-1);
	}
	cout<<dp[n];
	return 0;
}

小结:

发现死活优化不出来考虑有没有单调性,自己枚举情况推一下决策点范围是否和前面的决策点相关,若是推不出来就打表找规律

posted @ 2025-11-28 09:44  huhangqi  阅读(0)  评论(0)    收藏  举报
/*
*/