P4175 [CTSC2008]网络管理 题解
题目描述
给定一棵 \(n\) 个节点的树,点有点权 \(w_i\) 。 \(q\) 次操作:
0 u v:将节点 \(u\) 的权值修改为 \(v\) 。k u v:查询 \(u\to v\) 路径上第\(k\)大的点权,如果路径上不足 \(k\) 个点输出invalid request!。
数据范围
- \(1\le n,q\le 8\cdot 10^4,1\le k\le n,1\le w_i\le 10^8\) 。
时间限制 \(\texttt{2s}\) ,空间限制 \(\texttt{500MB}\) 。
分析
如果树退化成链,序列上区间第 \(k\) 大有两种经典做法:主席树或整体二分。
事实上这两种做法略加修改都可以解决本题。
树套树
主席树的瓶颈在于无法带修,考虑用树套树维护。
记 \(lca(u,v)=p\) ,那么 \(u\to v\) 的路径可以看成 \((u\to1)+(v\to1)-(p\to1)-(fa_p\to1)\) ,只需维护每个点到根的路径上所有点权的信息。
每次修改可以看成给原树的一棵子树删掉一个权值为 \(w_i\) 的点,再加入一个权值为 \(v\) 的点。
在 \(dfs\) 序上就是对区间进行操作,差分一下变成单点修改前缀求和,用树状数组套权值线段树实现。
查询时线段树二分,将 \(\mathcal O(\log n)\) 棵权值线段树的根节点同时向左或向右移动即可。
记得离散化,否则两只 \(\log\) 的空间存不下。
时间复杂度 \(\mathcal O((n+q)\log^2n)\) 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=8e4+5;
int m,n,q,u,v,cnt,tot;
int d[maxn],fa[maxn][17];
int w[maxn],sz[maxn],dfn[maxn];
int c[2*maxn],rt[maxn];
vector<int> g[maxn];
struct oper
{
int k,u,v;
}t[maxn];
struct node
{
int ls,rs,sum;
}f[500*maxn];
void dfs(int u,int f)
{
sz[u]=1,dfn[u]=++cnt;
for(auto v:g[u])
{
if(v==f) continue;
d[v]=d[u]+1,fa[v][0]=u;
for(int i=1;i<=16;i++) fa[v][i]=fa[fa[v][i-1]][i-1];
dfs(v,u),sz[u]+=sz[v];
}
}
int lca(int u,int v)
{
if(d[u]<d[v]) swap(u,v);
for(int i=16;i>=0;i--)
if(d[fa[u][i]]>=d[v])
u=fa[u][i];
if(u==v) return u;
for(int i=16;i>=0;i--)
if(fa[u][i]!=fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
void pushup(int p)
{
f[p].sum=f[f[p].ls].sum+f[f[p].rs].sum;
}
void modify(int &p,int l,int r,int pos,int val)
{
if(!p) p=++tot;
if(l==r) return f[p].sum+=val,void();
int mid=(l+r)/2;
if(pos<=mid) modify(f[p].ls,l,mid,pos,val);
else modify(f[p].rs,mid+1,r,pos,val);
pushup(p);
}
void add(int x,int v,int op)
{
while(x<=n) modify(rt[x],1,m,v,op),x+=x&(-x);
}
void ask(int x,vector<int> &vec)
{
while(x) vec.push_back(rt[x]),x-=x&(-x);
}
int query(int a,int b,int c,int d,int k)
{
vector<int> v1,v2;
ask(dfn[a],v1),ask(dfn[b],v1),ask(dfn[c],v2),ask(dfn[d],v2);
int l=1,r=m;
while(l!=r)
{
int cur=0,mid=(l+r)/2;
for(auto &p:v1) cur+=f[f[p].ls].sum;
for(auto &p:v2) cur-=f[f[p].ls].sum;
if(cur>=k)
{
r=mid;
for(auto &p:v1) p=f[p].ls;
for(auto &p:v2) p=f[p].ls;
}
else
{
l=mid+1,k-=cur;
for(auto &p:v1) p=f[p].rs;
for(auto &p:v2) p=f[p].rs;
}
}
return l;
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%d",&w[i]),c[++m]=w[i];
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v),g[v].push_back(u);
}
d[1]=1,dfs(1,0);
for(int i=1;i<=q;i++)
{
scanf("%d%d%d",&t[i].k,&t[i].u,&t[i].v);
if(!t[i].k) c[++m]=t[i].v;
}
sort(c+1,c+m+1);
m=unique(c+1,c+m+1)-c-1;
for(int i=1;i<=n;i++)
{
w[i]=lower_bound(c+1,c+m+1,w[i])-c;
add(dfn[i],w[i],1),add(dfn[i]+sz[i],w[i],-1);
}
for(int i=1;i<=q;i++)
{
int k=t[i].k,u=t[i].u,v=t[i].v;
if(!k)
{
add(dfn[u],w[u],-1),add(dfn[u]+sz[u],w[u],1);
w[u]=lower_bound(c+1,c+m+1,v)-c;
add(dfn[u],w[u],1),add(dfn[u]+sz[u],w[u],-1);
}
else
{
int p=lca(u,v),dis=d[u]+d[v]-2*d[p]+1;
if(k>dis) printf("invalid request!\n");
else printf("%d\n",c[query(u,v,p,fa[p][0],dis-k+1)]);
}
}
return 0;
}
整体二分
整体二分天然支持带修,所以相比之下码量会小很多。
整体二分为什么支持带修:
整体二分本质上是对值域做 \(cdq\) 分治。以动态区间第 \(k\) 小为例:
- 如果修改和询问都落在 \(mid\) 同一侧,直接往下递归即可。
- 如果修改 \(\le mid\) 但询问 \(\gt mid\) ,我们需要计算 \(\le mid\) 的修改对询问的贡献。
修改和询问还有时间维度上的偏序关系(前面的修改对后面的询问才会产生贡献),但刚好整体二分会顺序扫描所有操作,所以时间维度无需担心。
二分以后我们的操作会变成:单点加一减一,路径求和。
维护每个点到根的路径答案,那么单点加会对一棵子树产生贡献。
通过 \(dfs\) 序变成区间加,单点求值,差分树状数组解决。
时间复杂度 \(\mathcal O((n+q)\log n\log V)\) 。
#include<bits/stdc++.h>
using namespace std;
const int maxn=8e4+5;
int k,m,n,q,u,v,cnt;
int d[maxn],fa[maxn][17];
int sz[maxn],dfn[maxn];
int c[maxn],w[maxn],res[maxn];
vector<int> g[maxn];
struct oper
{
int k,p,u,v,id;
///id==0,k=1/-1表示加入/删除第u个点权值为v的贡献
///id!=0,询问u->v路径第k小,p=lca(u,v),编号为id
};
vector<oper> vec;
void dfs(int u,int f)
{
sz[u]=1,dfn[u]=++cnt;
for(auto v:g[u])
{
if(v==f) continue;
d[v]=d[u]+1,fa[v][0]=u;
for(int i=1;i<=16;i++) fa[v][i]=fa[fa[v][i-1]][i-1];
dfs(v,u),sz[u]+=sz[v];
}
}
int lca(int u,int v)
{
if(d[u]<d[v]) swap(u,v);
for(int i=16;i>=0;i--)
if(d[fa[u][i]]>=d[v])
u=fa[u][i];
if(u==v) return u;
for(int i=16;i>=0;i--)
if(fa[u][i]!=fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
void add(int x,int v)
{
while(x<=n) c[x]+=v,x+=x&(-x);
}
int ask(int x)
{
int res=0;
while(x) res+=c[x],x-=x&(-x);
return res;
}
void solve(int l,int r,vector<oper> vec)
{
if(vec.empty()) return ;
if(l==r)
{
for(auto p:vec) res[p.id]=l;
return ;
}
int mid=(l+r)/2;
vector<oper> v1,v2;
for(auto p:vec)
{
int &k=p.k,&u=p.u,&v=p.v;
if(!p.id)
{
if(v<=mid) add(dfn[u],k),add(dfn[u]+sz[u],-k),v1.push_back(p);
else v2.push_back(p);
}
else
{
int cur=ask(dfn[u])+ask(dfn[v])-ask(dfn[p.p])-ask(dfn[fa[p.p][0]]);
if(cur>=k) v1.push_back(p);
else k-=cur,v2.push_back(p);
}
}
for(auto p:v1)
{
int k=p.k,u=p.u;
if(!p.id) add(dfn[u],-k),add(dfn[u]+sz[u],k);
}
solve(l,mid,v1);
solve(mid+1,r,v2);
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%d",&w[i]),vec.push_back({1,0,i,w[i],0});
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v),g[v].push_back(u);
}
d[1]=1,dfs(1,0);
while(q--)
{
scanf("%d%d%d",&k,&u,&v);
if(!k) vec.push_back({-1,0,u,w[u],0}),w[u]=v,vec.push_back({1,0,u,w[u],0});
else
{
int p=lca(u,v),dis=d[u]+d[v]-2*d[p]+1;
if(k>dis) res[++m]=-1;
else vec.push_back({dis-k+1,p,u,v,++m});
}
}
solve(1,1e8,vec);
for(int i=1;i<=m;i++)
if(res[i]!=-1) printf("%d\n",res[i]);
else printf("invalid request!\n");
return 0;
}
树上带修莫队
其实这道题还可以用树上带修莫队做。
按照树上莫队的套路,先用欧拉序将树上路径变成区间内出现奇数次的数。
我们希望维护这个集合,考虑值域分块。
这样修改是 \(\mathcal O(1)\) 的,查询时先扫一遍可以求出第 \(k\) 小数所在块,再扫一遍即可求出具体值,时间复杂度 \(\mathcal O(\sqrt n)\) 。
处理修改多维护一维时间轴就可以了。
时间复杂度 \(\mathcal O(n^\frac 43q^\frac 13+q\sqrt n)\) 。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1.6e5+5,B=2000;
int l,m,n,q,r,t,u,v,cnt;
int d[maxn],fa[maxn][17];
int c[maxn],w[maxn],bel[maxn],res[maxn];
int id[maxn],st[maxn],ed[maxn];
pii h[maxn];
vector<int> g[maxn];
struct oper
{
int k,u,v;
}f[maxn];
struct quer
{
int k,p,l,r,t,id;
};
vector<quer> vec;
void dfs(int u,int f)
{
st[u]=++cnt,id[cnt]=u;
for(auto v:g[u])
{
if(v==f) continue;
d[v]=d[u]+1,fa[v][0]=u;
for(int i=1;i<=16;i++) fa[v][i]=fa[fa[v][i-1]][i-1];
dfs(v,u);
}
ed[u]=++cnt,id[cnt]=u;
}
int lca(int u,int v)
{
if(d[u]<d[v]) swap(u,v);
for(int i=16;i>=0;i--)
if(d[fa[u][i]]>=d[v])
u=fa[u][i];
if(u==v) return u;
for(int i=16;i>=0;i--)
if(fa[u][i]!=fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
bool cmp(quer a,quer b)
{
if(bel[a.l]!=bel[b.l]) return bel[a.l]<bel[b.l];
if(bel[a.r]!=bel[b.r]) return bel[a.r]<bel[b.r];
return a.t<b.t;
}
namespace block
{
const int B=400;
int n;
int a[maxn],bel[maxn],cnt[maxn];
int st[maxn],ed[maxn];
bool vis[maxn];
void init(int _n)
{
n=_n;
for(int i=1;i<=n;i++) bel[i]=(i-1)/B+1;
for(int i=1;i<=bel[n];i++) st[i]=(i-1)*B+1,ed[i]=min(i*B,n);
}
void add(int x)
{
int op=vis[x]?-1:1;
a[w[x]]+=op,cnt[bel[w[x]]]+=op,vis[x]^=1;
}
void work(int t)
{
int &x=h[t].fi,&y=h[t].se;
if(vis[x]) add(x),swap(w[x],y),add(x);
else swap(w[x],y);
}
int query(int k)
{
for(int i=1;i<=bel[n];i++)
{
if(k>cnt[i]) k-=cnt[i];
else
{
for(int j=st[i];j<=ed[i];j++)
if(k>a[j]) k-=a[j];
else return j;
return assert(false),-1;
}
}
return assert(false),-1;
}
}
using block::add;
using block::work;
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
g[u].push_back(v),g[v].push_back(u);
}
d[1]=1,dfs(1,0),cnt=0;
for(int i=1;i<=n;i++) c[++cnt]=w[i];
for(int i=1;i<=2*n;i++) bel[i]=(i-1)/B+1;
for(int i=1;i<=q;i++)
{
scanf("%d%d%d",&f[i].k,&f[i].u,&f[i].v);
if(!f[i].k) c[++cnt]=f[i].v;
}
sort(c+1,c+cnt+1);
cnt=unique(c+1,c+cnt+1)-c-1;
for(int i=1;i<=n;i++) w[i]=lower_bound(c+1,c+cnt+1,w[i])-c;
for(int i=1;i<=q;i++)
{
int k=f[i].k,u=f[i].u,v=f[i].v;
if(!k) h[++t]=mp(u,lower_bound(c+1,c+cnt+1,v)-c);
else
{
if(st[u]>st[v]) swap(u,v);
int p=lca(u,v),dis=d[u]+d[v]-2*d[p]+1;
m++;
if(k>dis) res[m]=-1;
else if(u==p) vec.push_back({dis-k+1,0,st[u],st[v],t,m});
else vec.push_back({dis-k+1,p,ed[u],st[v],t,m});
}
}
sort(vec.begin(),vec.end(),cmp);
l=1,r=0,t=0,block::init(cnt);
for(auto p:vec)
{
while(l>p.l) add(id[--l]);
while(r<p.r) add(id[++r]);
while(l<p.l) add(id[l++]);
while(r>p.r) add(id[r--]);
while(t<p.t) work(++t);
while(t>p.t) work(t--);
if(p.p) add(p.p);
res[p.id]=block::query(p.k);
if(p.p) add(p.p);
}
for(int i=1;i<=m;i++)
if(res[i]!=-1) printf("%d\n",c[res[i]]);
else printf("invalid request!\n");
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/17276826.html
浙公网安备 33010602011771号