ybtAu 「动态规划」第2章 数据结构优化 DP

这是 neatisaac 的金牌导航题解!

A. 【例题1】递增子序列

\(f_{i,j}\) 表示以 \(i\) 结尾,长度为 \(j\) 的递增子序列的个数,于是有:

\[\large f_{i,j}=\sum_{k=1,a_k<a_i}^{i-1}f_{k,j-1} \]

先枚举 \(j\),再枚举 \(i\),在树状数组上查询 \(\sum f_{k,j-1}\),并插入 \(f_{i,j-1}\)
需要离散化。

#include <iostream>
#include <algorithm>
#define N 10005
#define mod 123456789
int n,m,a[N],len,g[N];
std::pair<int,int> b[N];
struct BIT
{
	int f[N],r;
	void c(int x,int t) {for(;x<=len;x+=x&-x) (f[x]+=t)%=mod;}
	void d() {for(r=1;r<=len;r++) f[r]=0;}
	int q(int x) {for(r=0;x;x-=x&-x) (r+=f[x])%=mod;return r;}
} tr[105];
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	while(std::cin>>n>>m)
	{
		for(int i=1,x;i<=n;i++) g[i]=1,std::cin>>x,b[i]={x,i};
		std::sort(b+1,b+n+1);
		len=0;
		b[0].first=-1;
		for(int i=1;i<=n;i++)
		{
			if(b[i].first!=b[i-1].first) len++;
			a[b[i].second]=len;
		}
		for(int j=2;j<=m;j++)
		{
			tr[j].d();
			for(int i=1;i<=n;i++)
			{
				int tmp=g[i];
				g[i]=tr[j].q(a[i]-1),tr[j].c(a[i],tmp);
			}
		}
		int ans=0;
		for(int i=1;i<=n;i++) (ans+=g[i])%=mod;
		std::cout<<ans<<'\n';
	}
}

B. 基站选址

二分求出离第 \(i\) 个村庄最远且能覆盖 \(i\) 的村庄编号 \(l_i\)\(r_i\),并预处理出 \(r_j=i\) 的村庄。
\(f_{i,j}\) 表示在村庄 \(i\) 建立第 \(j\) 个基站,且不考虑 \(i\) 之后的村庄,所需的最小费用,\(cost(i,j)\) 表示对 \([i,j]\) 村庄的赔偿费用,于是有:

\[\large f_{i,j}=\min(f_{k,j-1}+cost(k,i)) \]

考虑用线段树来维护等号右面这坨东西。
对于区间 \([k,i]\) 中的村庄 \(x\),如果 \(k<l_x\),且 \(i>r_x\),那么就要加上这个村庄的赔偿费用,体现在线段树上就是对 \([1,l_x-1]\) 区间加 \(w_x\)
\(f_{i,j}\) 就体现为线段树上查询 \([1,i-1]\) 的区间最小值。
注意线段树的实现细节。

#include <iostream>
#include <cassert>
#define N 20005
int n,K,dis[N],c[N],s[N],w[N],rt,lc[N],rc[N],f[N];
int hed[N],tal[N],nxt[N],cnte;
void adde(int u,int v) {tal[++cnte]=v,nxt[cnte]=hed[u],hed[u]=cnte;}
namespace SGT
{
	int d[N<<2],tg[N<<2],ls[N<<2],rs[N<<2],idx;
	#define mid (lb+rb>>1)

	void mt(int x,int t) {tg[x]+=t,d[x]+=t;}
	void pd(int x) {if(tg[x]) mt(ls[x],tg[x]),mt(rs[x],tg[x]),tg[x]=0;}
	int build(int lb,int rb)
	{
		int x=++idx;
		tg[x]=0;
		if(lb==rb) return d[x]=f[lb],x;
		ls[x]=build(lb,mid),rs[x]=build(mid+1,rb);
		return d[x]=std::min(d[ls[x]],d[rs[x]]),x;
	}
	void md(int x,int l,int r,int t,int lb,int rb)
	{
		if(l<=lb&&rb<=r) {mt(x,t);return;}
		pd(x);
		if(l<=mid) md(ls[x],l,r,t,lb,mid);
		if(r>mid) md(rs[x],l,r,t,mid+1,rb);
		d[x]=std::min(d[ls[x]],d[rs[x]]);
	}
	int qr(int x,int l,int r,int lb,int rb)
	{
		if(l<=lb&&rb<=r) return d[x];
		pd(x);
		int ret=1e9;
		if(l<=mid) ret=qr(ls[x],l,r,lb,mid);
		if(r>mid) ret=std::min(ret,qr(rs[x],l,r,mid+1,rb));
		return ret;
	}
	#undef mid
};
int getl(int x)
{
	int l=1,r=n,ret=1;
	while(l<=r)
	{
		int mid=l+r>>1;
		(dis[mid]<=x)?(l=mid+1,ret=mid):(r=mid-1);
	}
	return ret;
}
int getr(int x)
{
	int l=1,r=n,ret=n;
	while(l<=r)
	{
		int mid=l+r>>1;
		(dis[mid]>=x)?(r=mid-1,ret=mid):(l=mid+1);
	}
	return ret;
}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>K;
	for(int i=2;i<=n;i++) std::cin>>dis[i];
	for(int i=1;i<=n;i++) std::cin>>c[i];
	for(int i=1;i<=n;i++) std::cin>>s[i];
	for(int i=1;i<=n;i++) std::cin>>w[i];
	dis[++n]=1e9,w[n]=1e9,K++;
	for(int i=1;i<=n;i++)
		lc[i]=getr(dis[i]-s[i]),rc[i]=getl(dis[i]+s[i]),adde(rc[i],i);
	int tmp=0;
	for(int i=1;i<=n;i++)
	{
		f[i]=tmp+c[i];
		for(int j=hed[i];j;j=nxt[j]) tmp+=w[tal[j]];
	}
	int ans=f[n];
	for(int k=2;k<=K;k++)
	{
		SGT::idx=0;
		rt=SGT::build(0,n);
		for(int i=1;i<=n;i++)
		{
			if(k>i) f[i]=c[i];
			else f[i]=c[i]+SGT::qr(rt,k-1,i-1,0,n);
			for(int j=hed[i];j;j=nxt[j]) SGT::md(rt,1,lc[tal[j]]-1,w[tal[j]],0,n);
		}
		ans=std::min(ans,f[n]);
	}
	std::cout<<ans;
}

C. 折线统计

将点按 \(x\) 排序,令 \(f_{i,j,0/1}\) 表示以 \(i\) 结尾,共 \(j\) 段,最后一段是上升(1)/下降(0)的集合数,于是有:

\[\large f_{i,j,1}=\sum_{k=1,y_k<y_i}^{i-1}f_{k,j-1,0}+f_{k,j,1} \\ f_{i,j,0}=\sum_{k=1,y_k>y_i}^{i-1}f_{k,j-1,1}+f_{k,j,0} \]

可以使用树状数组来维护 \(f\),需要对 \(y\) 离散化。

#include <iostream>
#include <algorithm>
#define N 100005
#define mod 100007
int n,k,len;
struct BIT
{
	int f[N],r;
	void c(int x,int t) {for(;x<=len;x+=x&-x) (f[x]+=t)%=mod;}
	int q(int x) {for(r=0;x;x-=x&-x) (r+=f[x])%=mod;return r;}
} tr[15][2];
int f[N][15][2];
std::pair<int,int> a[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,x,y;i<=n;i++) std::cin>>x>>y,a[i]={x,y},len=std::max(len,y);
	std::sort(a+1,a+n+1);
	for(int i=1;i<=n;i++)
	{
		int y=a[i].second;
		f[i][0][1]=f[i][0][0]=1;
		tr[0][1].c(y,1),tr[0][0].c(y,1);
		for(int j=1;j<=k;j++)
		{
			(f[i][j][1]+=tr[j-1][0].q(y-1)+tr[j][1].q(y-1))%=mod;
			(f[i][j][0]+=tr[j-1][1].q(len)-tr[j-1][1].q(y)+tr[j][0].q(len)-tr[j][0].q(y))%=mod;
			tr[j][0].c(y,f[i][j][0]),tr[j][1].c(y,f[i][j][1]);
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++) (ans+=f[i][k][0]+f[i][k][1])%=mod;
	std::cout<<(ans+mod)%mod;
}

D. 免费馅饼

\(f_i\) 表示前 \(i\) 个馅饼中,保证接到第 \(i\) 个馅饼的最大分数和,容易得出:

\[\large f_i=\sum_{k=1}^{i-1}{f_k}+v_i(|p_k-p_i|\le2(t_i-t_k)) \]

对括号中式子展开得:

\[\large 2t_k-p_k\le 2t_i-p_i \\ 2t_k+p_k\le 2t_i-p_i \]

显然,当两式同时成立就能消除 \(t\) 的影响。
于是变成了一个二维偏序问题。
可以按照 \(2t_i-p_i\) 排序,并用树状数组来维护 \(2t_i+p_i\)。注意要对 \(2t_i+p_i\) 进行离散化。

#include <iostream>
#include <algorithm>
#define N 100005
int w,n,len;
std::pair<std::pair<int,int>,int> a[N];
std::pair<int,int> b[N];
struct BIT
{
	int f[N],r;
	void c(int x,int t) {for(;x<=len;x+=x&-x) f[x]=std::max(f[x],t);}
	int q(int x) {for(r=0;x;x-=x&-x) r=std::max(r,f[x]);return r;}
} tr;
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>w>>n;
	for(int i=1,t,p,v;i<=n;i++)
		std::cin>>t>>p>>v,a[i]={{2*t-p,2*t+p},v},b[i]={2*t+p,i};
	std::sort(b+1,b+n+1);
	for(int i=1;i<=n;i++)
	{
		if(b[i].first!=b[i-1].first) len++;
		a[b[i].second].first.second=len;
	}
	std::sort(a+1,a+n+1);
	int ans=0;
	for(int i=1;i<=n;i++)
	{
		int tmp=tr.q(a[i].first.second)+a[i].second;
		ans=std::max(ans,tmp);
		tr.c(a[i].first.second,tmp);
	}
	std::cout<<ans;
}

E. 优美玉米

观察到每次拔高的区间右端点设为 \(n\) 总是不劣的。
\(f_{i,j}\) 表示第 \(i\) 个玉米被拔高 \(j\) 次时的最长不降子序列,有:

\[\large f_{i,j}=\max_{k<i,a_k+x\le a_i+j}f_{k,x} \]

变成了一个二维前缀最大值问题,可以用树状数组维护。

#include <iostream>
#define N 10005
#define M 505
int n,K,a[N],len,ans;
struct BIT
{
	int f[N][M],r,i,j;
	void c(int x,int y,int t) {for(i=x;i<=len;i+=i&-i) for(j=y;j<=K+1;j+=j&-j) f[i][j]=std::max(f[i][j],t);}
	int q(int x,int y) {for(r=0,i=x;i;i-=i&-i) for(j=y;j;j-=j&-j) r=std::max(r,f[i][j]);return r;}
} tr;
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],len=std::max(len,a[i]+K);
	for(int i=1;i<=n;i++) for(int j=K+1;j;j--)
	{
		int tmp=tr.q(a[i]+j-1,j)+1;
		ans=std::max(ans,tmp);
		tr.c(a[i]+j-1,j,tmp);
	}
	std::cout<<ans;
}

F. 地精部落

要求一个长度与值域都为 \(n\) 的满足一大一小一大一小的序列个数。
做法十分玄学:
我们令 \(f_{i,j}\) 表示第 \(i\) 个数是 \(j\) 且为山峰的方案数,然后开始讨论。
\(j\)\(j-1\) 不相邻时,我们发现交换一个合法序列的两个不相邻的数,这个序列还是合法的。于是 \(f_{i,j}\leftarrow f_{i,j-1}\)
\(j\)\(j-1\) 相邻时,显然 \(j\) 为山峰 \(j-1\) 为山谷,而我们发现,一个合法序列上下翻转,得到的序列还是合法的。于是 \(j-1\) 为山谷的方案数就等于 \((i-1)-(j-1)+1\) 的方案数,即 \(f_{i,j}\leftarrow f_{i-1,i-j+1}\)
初始状态 \(f_{2,2}=1\),因为 \(1\) 不能是山峰。答案就是 \(2\sum f_{n,i}\)
卡空间,需要滚动数组优化。

#include <iostream>
#define N 4205
int f[2][N],n,mod;
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>mod;
	f[0][2]=1;
	for(int i=3,d=1;i<=n;i++,d^=1) for(int j=2;j<=n;j++) f[d][j]=(f[d][j-1]+f[d^1][i-j+1])%mod;
	int ans=0;
	for(int i=1;i<=n;i++) (ans+=f[n&1][i])%=mod;
	std::cout<<ans*2%mod;
}

\[\Huge End \]

posted @ 2025-05-04 15:36  整齐的艾萨克  阅读(19)  评论(0)    收藏  举报