世界树(HNOI2014)
世界树(HNOI2014)
题目描述
给出一棵有\(n\)个点,边权全部为\(1\)的树,有\(q\)个询问。
对于每个询问,都会给出树中的\(m\)个点,对于所有的\(n\)个点,每个点都会附属于这\(m\)个点中距离它本身最近的点(若满足条件,可以附属于自身上),要求你求出对于这\(m\)个点分别有多少个节点附属于它。
\(\sum{m}<=300000\)
思路
这题其实(据说)用虚树写蛮简单,但由于本人较弱不会,讲一个\(dfs序+线段树+倍增LCA+离线\)的方法。。。
首先是预处理倍增,LCA,\(dfs\)序等,这些不多说。接下来我们做这样的操作:把每次询问的\(m\)个点按照点的深度排序。如果按此序依次放入每一个点,就不可能在该点的子树中存在之前放入的点。至于这个小性质有啥用,我们待会在说。。
我们考虑放入一个点后,对那些点的附属值产生影响。这时排序的作用就来了:在这点变成可附属点时,它原本就会有一个它的附属点,那么这样排序就只会影响该点的附属值,说简单点就是:当\(u\)原本附属于\(v\)时,当\(u\)变为可附属点时,只会影响\(v\)的附属值。如何证明应该不用赘述吧!应该画个图就可以理解了,注意是按深度排序的就行了!
接下来就是\(u\)会抢走\(v\)的几个点呢?同理,通过画图以及人类智慧可知,正是从\(u\)到\(v\)的路径的中心点\(w\)的子树,由于\(u\)深度大于\(v\)深度,所以\(w\)肯定是\(u\)的祖先,所以就要用倍增跳\(u\),跳到\(w\),然而注意,如果\(w\)到\(u\),\(v\)的路径相等时,还要特判两者的编号大小并以此决定向上跳的路径。路径长度便需要\(LCA\)求解。
最后就是如何维护每个点的附属点,这个就不复杂了,由于每次抢走的就是一个子树,所以只要\(dfs\)序区间更新在线段树上即可,若是第一个点(即一开始没可附属点时),就把所有的点(即1~n区间)更新了。
这样综合了之后,这题便得到圆满解决,还有什么细节详见代码。
代码
#include<bits/stdc++.h>
#define FOR(i,l,r) for(int i=l,i##R=r;i<=i##R;i++)
#define DOR(i,r,l) for(int i=r,i##L=l;i>=i##L;i--)
#define loop(i,n) for(int i=0,i##R=n;i<i##R;i++)
#define sf scanf
#define pf printf
#define mms(a,x) memset(a,x,sizeof a)
using namespace std;
typedef long long ll;
typedef long double db;
template<typename A,typename B>inline void chkmax(A &x,const B y){if(x<y)x=y;}
template<typename A,typename B>inline void chkmin(A &x,const B y){if(x>y)x=y;}
const int N=3e5+5;
int n,m,q;
struct Graph{//正向表
int tot,to[N<<1],nxt[N<<1],head[N];
void add(int x,int y){tot++;to[tot]=y;nxt[tot]=head[x];head[x]=tot;}
void clear(){mms(head,-1);tot=0;}
#define EOR(G,i,x) for(int i=G.head[x];i!=-1;i=G.nxt[i])
}G;
int cnt[N];
int ans[N];
struct node{
int x,id;
}A[N];
int ct,sz[N],top[N],son[N],dep[N],fa[N][20];
int dfn[N],post[N];
bool cmp(node a,node b){//先按深度排,再按编号排,不能忽略编号
if(dep[a.x]!=dep[b.x])return dep[a.x]<dep[b.x];
return a.x<b.x;
}
struct Pt2{
struct YD_Tree{//维护每个点的附属点
#define ls (p<<1)
#define rs (p<<1|1)
static const int M=(N<<2);
int bel[M];
void down(int p){
if(!bel[p])return;
bel[ls]=bel[rs]=bel[p];
bel[p]=0;
}
void update(int p,int l,int r,int L,int R,int x){
if(L<=l&&r<=R){
bel[p]=x;
return;
}
down(p);
int mid=(l+r)>>1;
if(mid<R)update(rs,mid+1,r,L,R,x);
if(mid>=L)update(ls,l,mid,L,R,x);
}
int query(int p,int l,int r,int pos){
if(bel[p])return bel[p];
int mid=(l+r)>>1;
if(mid<pos)return query(rs,mid+1,r,pos);
else return query(ls,l,mid,pos);
}
}Tr;
void dfs(int x,int f){
fa[x][0]=f;
dfn[x]=++ct;
dep[x]=dep[f]+1;
sz[x]=1,son[x]=0;
EOR(G,i,x){
int v=G.to[i];
if(v==f)continue;
dfs(v,x);
sz[x]+=sz[v];
if(sz[son[x]]<sz[v])son[x]=v;
}
post[x]=ct;
}
void top_dfs(int x,int f,int tp){
top[x]=tp;
if(son[x])top_dfs(son[x],x,tp);
EOR(G,i,x){
int v=G.to[i];
if(v==f||v==son[x])continue;
top_dfs(v,x,v);
}
}
int LCA(int a,int b){//个人用了跳重链,常数较小,嫌烦的直接倍增完全没问题
while(top[a]!=top[b]){
if(dep[top[a]]>dep[top[b]])a=fa[top[a]][0];
else b=fa[top[b]][0];
}
return dep[a]>dep[b]?b:a;
}
void jump(int &x,int len){//倍增跳点
FOR(i,0,19)
if((len>>i)&1)x=fa[x][i];
}
void solve(){
sf("%d",&q);
dfs(1,0);
top_dfs(1,0,1);
FOR(j,1,19)FOR(i,1,n)
fa[i][j]=fa[fa[i][j-1]][j-1];
while(q--){
sf("%d",&m);
FOR(i,1,m){
sf("%d",&A[i].x);
A[i].id=i;
ans[i]=0,cnt[A[i].x]=0;
}
sort(A+1,A+m+1,cmp);
Tr.update(1,1,n,1,n,A[1].x);//第一个点放入时,所有点都附属于它
cnt[A[1].x]=n;
FOR(i,2,m){
int x=A[i].x;
int last=Tr.query(1,1,n,dfn[x]);//原附属点
int lca=LCA(x,last);
int dis=dep[last]+dep[x]-(dep[lca]<<1);
jump(x,(dis-(x>last))>>1);
// pf("after jumped:%d sz:%d\n",x,sz[x]);
cnt[A[i].x]+=sz[x];
cnt[last]-=sz[x];
Tr.update(1,1,n,dfn[x],post[x],A[i].x);
}
FOR(i,1,m)ans[A[i].id]=cnt[A[i].x];
FOR(i,1,m)pf("%d ",ans[i]);puts("");
}
}
}Pt_2;
int main(){
freopen("worldtree.in","r",stdin);
freopen("worldtree.out","w",stdout);
G.clear();
sf("%d",&n);
FOR(i,1,n-1){
int x,y;
sf("%d%d",&x,&y);
G.add(x,y),G.add(y,x);
}
Pt_2.solve();
return 0;
}