ybtAu 「动态规划」第4章 单调队列优化 DP

这是neatisaac的金牌导航题解!

A. 【例题1】滑动窗口

略。

#include <iostream>
#define N 1000005
int n,k,a[N],q[N];
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>k;
	for(int i=1;i<=n;i++) std::cin>>a[i];
	int hd=1,tl=0;
	for(int i=1;i<=n;i++)
	{
		while(hd<=tl&&q[hd]<=i-k) hd++;
		while(hd<=tl&&a[q[tl]]>a[i]) tl--;
		q[++tl]=i;
		if(i>=k) std::cout<<a[q[hd]]<<' ';
	}
	std::cout<<'\n';
	hd=1,tl=0;
	for(int i=1;i<=n;i++)
	{
		while(hd<=tl&&q[hd]<=i-k) hd++;
		while(hd<=tl&&a[q[tl]]<a[i]) tl--;
		q[++tl]=i;
		if(i>=k) std::cout<<a[q[hd]]<<' ';
	}
}

B. 【例题2】最大连续和

\(f_i\) 表示以 \(i\) 为结尾的最大连续和,\(A_i\) 表示前 \(i\) 个数的和,有:

\[\large f_i=\max_{j=i-m}^{i-1}(A_i-A_j) \\\large =A_i-\min_{j=i-m}^{i-1}A_j \]

单调队列维护即可。

#include <iostream>
#define N 200005
int n,m,a[N],ans,q[N];
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m;
	for(int i=1;i<=n;i++) std::cin>>a[i],a[i]+=a[i-1];
	int hd=1,tl=1;
	ans=-1e9;
	for(int i=1;i<=n;i++)
	{
		while(hd<=tl&&q[hd]<=i-m-1) hd++;
		if(hd<=tl) ans=std::max(ans,a[i]-a[q[hd]]);
		while(hd<=tl&&a[q[tl]]>a[i]) tl--;
		q[++tl]=i;
	}
	std::cout<<ans;
}

C. 【例题3】修剪草坪

不能超过 \(K\) 只连续的奶牛,那么每两个长度不超过 \(K\) 的连续段之间需要空一个。

\(f_i\) 表示前 \(i\) 只奶牛的最大效率,\(E_i\) 表示前缀和,有:

\[\large f_i=\max_{j=i-k}^{i-1}(f_{j-1}+E_i-E_j) \]

#include <iostream>
#define N 100005
#define int long long
int n,k,a[N],f[N],q[N],ans;
int W(int x) {return f[(x-1<0)?0:(x-1)]-a[x];}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>k;
	int hd=1,tl=1;
	for(int i=1;i<=n;i++) std::cin>>a[i],a[i]+=a[i-1];
	for(int i=1;i<=n;i++)
	{
		while(hd<=tl&&q[hd]<i-k) hd++;
		f[i]=W(q[hd])+a[i],ans=std::max(ans,f[i]);
		while(hd<=tl&&W(q[tl])<W(i)) tl--;
		q[++tl]=i;
	}
	std::cout<<ans;
}

D. 【例题4】岛屿

好题,但是金牌导航没有给出实现。

题意

\(N\) 个点 \(N\) 条边的图,求每个连通块内最长链的长度和。

做法

由于这是一个 \(N\) 个点 \(N\) 条边的图,且保证了每个点的出度为 \(1\),所以这是一个外向基环树森林。

对于其中的每一颗基环树,最长链有三种:

  • 从叶子节点指向所在树的根的链;
  • 在一棵树内,从一个叶子节点到另一个叶子节点的链;
  • 从一个叶子节点,先到根节点,再在环上走几条边,最后到另一棵树的一个叶子节点。

前两种较为好求,而对于第三种,由于有一部分在环上,我们需要对环进行处理。

具体地,先断环为链,再将这条链复制一次,这样就能保证原来环上任意一段都能在这条链上被找到。

\(f_i\) 表示从 \(i\) 的子树到 \(i\) 前某点的子树中最大的链长度,\(d_i\) 表示 \(i\) 的子树中到 \(i\) 距离最远的节点到 \(i\) 的距离,\(a_i\) 表示 \(i\) 与它前面一个点的边的长度,\(len\) 表示环上节点个数,于是有:

\[\large f_i=\max_{j=i-len+1}^{i-1}(d_j+d_i+\sum_{k=j+1}^{i}a_k) \]

变成了一个单调队列优化 DP 板子。

细节

注意长度为 \(2\) 的环也是环,所以不能随便建双向边,而是建出原图和反图,在原图上找节点到根的距离,在反图上找环。

求解第二种最长链的时候需要在 DFS 时存储最大值和次大值。

实现

#include <iostream>
#define N 2000005
int hed[N],deh[N],tal[N],nxt[N],cnte;
long long wt[N],b[N],c[N],d[N],ans,mx;
void adde(int u,int v,int w) {tal[++cnte]=v,wt[cnte]=w,nxt[cnte]=hed[u],hed[u]=cnte;}
void edda(int u,int v,int w) {tal[++cnte]=v,wt[cnte]=w,nxt[cnte]=deh[u],deh[u]=cnte;}
int n,st[N],dfn[N],li[N],q[N],tp,len,tsiz;
bool vis[N],in[N];
long long getdis(int x,long long dis)
{
	long long ret=dis,semi=-1e18;
	in[x]=vis[x]=1;
	for(int i=deh[x];i;i=nxt[i]) if(!in[tal[i]])
	{
		long long t=getdis(tal[i],dis+wt[i]);
		if(t>=ret) semi=ret,ret=t;
		else if(t>=semi) semi=t;
	}
	mx=std::max(mx,ret+semi-dis*2);
	return ret;
}
void dfs(int x)
{
	st[dfn[x]=++tp]=x,vis[x]=1;
	for(int i=hed[x];i;i=nxt[i])
	{
		if(vis[tal[i]])
		{
			for(int j=dfn[tal[i]];j<=dfn[x];j++,tsiz++) li[++len]=st[j],c[len]=b[st[j]],in[st[j]]=1;
			for(int j=dfn[tal[i]];j<=dfn[x];j++) li[++len]=st[j];
			c[tsiz+1]=wt[i];
		}
		else b[tal[i]]=wt[i],dfs(tal[i]);
		tp=dfn[x];
	}
}
long long solve(int x)
{
	int hd=1,tl=1;
	q[1]=1;
	mx=0;
	len=tsiz=tp=0;
	dfs(x);
	for(int i=1;i<=tsiz;i++) d[i]=getdis(li[i],0);
	for(int i=tsiz+1;i<=len;i++) d[i]=d[i-tsiz];
	for(int i=tsiz+2;i<=len;i++) c[i]=c[i-tsiz];
	for(int i=2;i<=len;i++) c[i]+=c[i-1];
	for(int i=2;i<=len;i++)
	{
		while(hd<=tl&&q[hd]<=i-tsiz) hd++;
		long long tmp=d[q[hd]]+d[i]+c[i]-c[q[hd]];
		mx=std::max(mx,tmp);
		while(hd<=tl&&d[q[tl]]-c[q[tl]]<d[i]-c[i]) tl--;
		q[++tl]=i;
	}
	return mx;
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n;
	for(int i=1,v,w;i<=n;i++) std::cin>>v>>w,adde(i,v,w),edda(v,i,w);
	for(int i=1;i<=n;i++) if(!vis[i]) ans+=solve(i);
	std::cout<<ans;
}

E. 【例题5】旅行问题

断环成链,然后求前后缀最小值,看是否大于 \(0\)然而 neatissac 没有想出来

#include <iostream>
#define int long long
#define N 2000005
int n,p[N],d[N],q[N],s[N];
bool f[N];
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n;
	for(int i=1;i<=n;i++) std::cin>>p[i]>>d[i];
	for(int i=n+1;i<=n*2;i++) p[i]=p[i-n],d[i]=d[i-n];
	for(int i=1;i<=n*2;i++) s[i]=s[i-1]+p[i]-d[i];
	int hd=1,tl=1;
	for(int i=1;i<n*2;i++)
	{
		while(hd<=tl&&q[hd]<=i-n) hd++;
		while(hd<=tl&&s[q[tl]]>s[i]) tl--;
		q[++tl]=i;
		if(i>=n) if(s[q[hd]]>=s[i-n]) f[i-n+1]=1;
	}
	hd=tl=1;
	for(int i=n*2;i>=1;i--) s[i]=s[i+1]+p[i]-d[i-1];
	q[1]=n*2+1;
	for(int i=n*2;i>=2;i--)
	{
		while(hd<=tl&&q[hd]>=i+n) hd++;
		while(hd<=tl&&s[q[tl]]>s[i]) tl--;
		q[++tl]=i;
		if(i<=n+1) if(s[q[hd]]>=s[i+n]) f[i-1]=1;
	}
	for(int i=1;i<=n;i++) std::cout<<((f[i])?"TAK\n":"NIE\n");
}

F. 【例题6】货币系统

单调队列优化多重背包模板。

#include <iostream>
#define N 20005
int n,m,b[N],c[N],f[205][N],q[N];
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n;
	for(int i=1;i<=n;i++) std::cin>>b[i];
	for(int i=1;i<=n;i++) std::cin>>c[i];
	std::cin>>m;
	for(int i=1;i<=m;i++) f[0][i]=1e9;
	for(int i=1;i<=n;i++)
		for(int j=0;j<b[i];j++)
		{
			int hd=1,tl=0;
			for(int k=0;j+k*b[i]<=m;k++)
			{
				while(hd<=tl&&q[hd]<k-c[i]) hd++;
				while(hd<=tl&&f[i-1][j+q[tl]*b[i]]-q[tl]>f[i-1][j+k*b[i]]-k) tl--;
				q[++tl]=k;
				f[i][j+k*b[i]]=f[i-1][j+q[hd]*b[i]]+k-q[hd];
			}
		}
	std::cout<<f[n][m];
}

G. 瑰丽华尔兹

这是我第三次遇到这道题。

第一次是九月,我写了个暴搜。

第二次是二月,我过了。

第三次是五月。

我们__倒过来求__,令 \(f_{i,j,k}\) 表示第 \(i\) 个时间段结束时,在 \((j,k)\) 能得到的最大分数,\(D_i\) 表示第 \(i\) 个时间段的长度。有:

\[\large f_{i,j,k}=\max_{y=k-D_i}^{k}(f_{i+1,j,y}+k-y)(d=1) \\\large f_{i,j,k}=\max_{y=k}^{k+D_i}(f_{i+1,j,y}+y-k)(d=2) \\\large f_{i,j,k}=\max_{x=j-D_i}^{j}(f_{i+1,x,k}+j-x)(d=3) \\\large f_{i,j,k}=\max_{y=j}^{j+D_i}(f_{i+1,x,k}+x-j)(d=4) \]

#include <iostream>
#define N 205
int n,m,X,Y,K,f[N][N][N],t[N],d[N],q[N];
bool a[N][N];
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m>>X>>Y>>K;
	for(int i=1;i<=n;i++)
	{
		std::string s;
		std::cin>>s;
		for(int j=0;j<m;j++) a[i][j+1]=s[j]=='.';
	}
	for(int i=1,st,ed;i<=K;i++) std::cin>>st>>ed>>d[i],t[i]=ed-st+1;
	for(int i=K;i>=1;i--)
	{
		int T=t[i],D=d[i];
		if(D==1) for(int j=1;j<=m;j++)
		{
			int hd=1,tl=0;
			for(int k=1;k<=n;k++)
			{
				if(!a[k][j]) {hd=1,tl=0;continue;}
				while(hd<=tl&&q[hd]<k-T) hd++;
				while(hd<=tl&&f[i+1][q[tl]][j]-q[tl]<f[i+1][k][j]-k) tl--;
				q[++tl]=k;
				f[i][k][j]=f[i+1][q[hd]][j]+k-q[hd];
			}
		}
		if(D==2) for(int j=1;j<=m;j++)
		{
			int hd=1,tl=0;
			for(int k=n;k>=1;k--)
			{
				if(!a[k][j]) {hd=1,tl=0;continue;}
				while(hd<=tl&&q[hd]>k+T) hd++;
				while(hd<=tl&&f[i+1][q[tl]][j]+q[tl]<f[i+1][k][j]+k) tl--;
				q[++tl]=k;
				f[i][k][j]=f[i+1][q[hd]][j]+q[hd]-k;
			}
		}
		if(D==3) for(int j=1;j<=n;j++)
		{
			int hd=1,tl=0;
			for(int k=1;k<=m;k++)
			{
				if(!a[j][k]) {hd=1,tl=0;continue;}
				while(hd<=tl&&q[hd]<k-T) hd++;
				while(hd<=tl&&f[i+1][j][q[tl]]-q[tl]<f[i+1][j][k]-k) tl--;
				q[++tl]=k;
				f[i][j][k]=f[i+1][j][q[hd]]+k-q[hd];
			}
		}
		if(D==4) for(int j=1;j<=n;j++)
		{
			int hd=1,tl=0;
			for(int k=m;k>=1;k--)
			{
				if(!a[j][k]) {hd=1,tl=0;continue;}
				while(hd<=tl&&q[hd]>k+T) hd++;
				while(hd<=tl&&f[i+1][j][q[tl]]+q[tl]<f[i+1][j][k]+k) tl--;
				q[++tl]=k;
				f[i][j][k]=f[i+1][j][q[hd]]+q[hd]-k;
			}
		}
	}
	std::cout<<f[1][X][Y];
}

H. 理想方形

二维滑动窗口。

先横着求每行每个位置的长度不超过 \(n\) 的最长子段和 \(b_{i,j}\),再竖着对 \(b_{i,j}\) 做如上求解,求每个正方形的最大值和最小值。

#include <iostream>
#include <cstring>
#define N 1005
int n,m,K,a[N][N],mn1[N][N],mn2[N][N],mx1[N][N],mx2[N][N],q[N];
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m>>K;
	memset(mn1,0x3f,sizeof mn1);
	memset(mn2,0x3f,sizeof mn2);
	for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) std::cin>>a[i][j];
	for(int i=1;i<=n;i++)
	{
		int hd=1,tl=0;
		for(int j=1;j<=m;j++)
		{
			while(hd<=tl&&q[hd]<=j-K) hd++;
			while(hd<=tl&&a[i][q[tl]]>a[i][j]) tl--;
			q[++tl]=j;
			mn1[i][j]=a[i][q[hd]];
		}
		hd=1,tl=0;
		for(int j=1;j<=m;j++)
		{
			while(hd<=tl&&q[hd]<=j-K) hd++;
			while(hd<=tl&&a[i][q[tl]]<a[i][j]) tl--;
			q[++tl]=j;
			mx1[i][j]=a[i][q[hd]];
		}
	}
	for(int j=1;j<=m;j++)
	{
		int hd=1,tl=0;
		for(int i=1;i<=n;i++)
		{
			while(hd<=tl&&q[hd]<=i-K) hd++;
			while(hd<=tl&&mn1[q[tl]][j]>mn1[i][j]) tl--;
			q[++tl]=i;
			mn2[i][j]=mn1[q[hd]][j];
		}
		hd=1,tl=0;
		for(int i=1;i<=n;i++)
		{
			while(hd<=tl&&q[hd]<=i-K) hd++;
			while(hd<=tl&&mx1[q[tl]][j]<mx1[i][j]) tl--;
			q[++tl]=i;
			mx2[i][j]=mx1[q[hd]][j];
		}
	}
	int ans=1e9;
	for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
		if(i>=K&&j>=K) ans=std::min(ans,mx2[i][j]-mn2[i][j]);
	std::cout<<ans;
}

I. 股票交易

\(f_{i,j}\) 表示第 \(i\) 天手里有 \(j\) 只股票的最大赚钱数。

有四种情况:

  • 啥也不干;

  • 直接买;

  • 在第 \(i-W-1\) 天的基础上卖;

  • 在第 \(i-W-1\) 天的基础上买。

\[\large f_{i,j}\leftarrow f_{i-1,j} \\\large f_{i,j}\leftarrow-jAP_i(j\le AS_i) \\\large f_{i,j}\leftarrow\min_{k}(f_{i-W-1,k}+(k-j)BP_i)(j<k\le j+BS_i) \\\large f_{i,j}\leftarrow\min_{k}(f_{i-W-1,k}-(j-k)AP_i)(j-AS_i\le k<j) \]

单调队列维护即可。

#include <iostream>
#include <list>
#include <cstring>
#define N 2005
int n,m,w,f[N][N];
std::list<int> q;
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>m>>w;
	memset(f,-0x3f,sizeof f);
	f[0][0]=0;
	for(int i=1;i<=n;i++)
	{
		int ap,bp,as,bs;
		std::cin>>ap>>bp>>as>>bs;
		for(int j=0;j<=m;j++)
		{
			f[i][j]=f[i-1][j];
			if(j<=as) f[i][j]=std::max(f[i][j],-j*ap);
		}
		if(i<=w) continue;
		q=std::list<int>();
		for(int j=0;j<=m;j++)
		{
			while(!q.empty()&&q.front()<j-as) q.pop_front();
			while(!q.empty()&&f[i-w-1][q.back()]+q.back()*ap<=f[i-w-1][j]+j*ap) q.pop_back();
			q.push_back(j);
			f[i][j]=std::max(f[i][j],f[i-w-1][q.front()]+(q.front()-j)*ap);
		}
		q=std::list<int>();
		for(int j=m;j>=0;j--)
		{
			while(!q.empty()&&q.front()>j+bs) q.pop_front();
			while(!q.empty()&&f[i-w-1][q.back()]+q.back()*bp<f[i-w-1][j]+j*bp) q.pop_back();
			q.push_back(j);
			f[i][j]=std::max(f[i][j],f[i-w-1][q.front()]+(q.front()-j)*bp);
		}
	}
	int ans=-1e9;
	for(int i=0;i<=m;i++) ans=std::max(ans,f[n][i]);
	std::cout<<ans;
}
posted @ 2025-05-12 15:18  整齐的艾萨克  阅读(6)  评论(0)    收藏  举报