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\) 个数的和,有:
单调队列维护即可。
#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\) 表示前缀和,有:
#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\) 表示环上节点个数,于是有:
变成了一个单调队列优化 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\) 个时间段的长度。有:
#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\) 天的基础上买。
单调队列维护即可。
#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;
}

浙公网安备 33010602011771号