ybtAu 「动态规划」第2章 数据结构优化 DP
这是 neatisaac 的金牌导航题解!
A. 【例题1】递增子序列
令 \(f_{i,j}\) 表示以 \(i\) 结尾,长度为 \(j\) 的递增子序列的个数,于是有:
先枚举 \(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]\) 村庄的赔偿费用,于是有:
考虑用线段树来维护等号右面这坨东西。
对于区间 \([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)的集合数,于是有:
可以使用树状数组来维护 \(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\) 个馅饼的最大分数和,容易得出:
对括号中式子展开得:
显然,当两式同时成立就能消除 \(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\) 次时的最长不降子序列,有:
变成了一个二维前缀最大值问题,可以用树状数组维护。
#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;
}

浙公网安备 33010602011771号