【題解/學習筆記】點分樹
\(\text{Solution:}\)
是點分樹的模板,這裡講一講點分樹。
本質就是把點分治的每一層分治重心給記錄下來了,自然就形成了一棵樹,並且樹高是 \(O(\log n)\) 的。這很顯然。
那麼,考慮點分治的過程,實際上就是從點分樹從根往下計算答案的過程了。
如果我們要計算點 \(x\) 的答案,對應地,應該是從根分治到點 \(x,\) 逆過來,就是從 \(x\) 跳到點分樹的根。
所以,我們對答案維護只需要從這個點往上跳就好了。
那麼我們考慮這個過程中哪些點對答案有貢獻:
顯然是跳到的點子樹內距離根為 \(y-dis(x,now)\) 的點。因它們可以拼成一條長 \(y\)
所以我們只需要對每個子樹維護這個東西即可。一個是維護自己子樹內部對它自己的貢獻,另一個需要維護自己子樹內單點對其父親的貢獻。
於是,應用容斥原理,計算其父親子樹內除 \(x\) 子樹外其他點對它的貢獻就是用總貢獻減去 \(x\) 子樹內對其父親貢獻為 \(y-dis\) 的部分。
而我們每次詢問的是一個字首和,所以可以樹狀陣列維護。
由於點分樹結構優秀,所以暴力跳的複雜度就是對的了。
同時,注意到點分樹上的結構和原樹的還是差別很大,所以我們需要在原樹上面獲得兩點距離一類資訊,而計算距離需要找 \(LCA,\) 利用尤拉序 \(st\) 表的科技就可以做到 \(O(n\log n)\)
總之,點分樹有以下特點:
-
對應每個節點是點分治的分治重心
-
樹的結構非常均勻,樹高是 \(O(\log n)\)
-
樹的結構與原樹關係很小,很多資訊需要在原樹裡面獲得
-
需要注意原樹和點分樹的差異,從而思考清楚兩部分的資訊處理
-
然後考慮答案所求在點分樹上如何統計,從而確定如何統計資訊,需要維護哪些資訊
-
進而思考如何維護資訊
大概就是這樣的流程。至於修改操作:之所以建立點分樹就是因為修改使得每次需要重複找重心進行計算,但這些修改對樹結構沒有影響,所以這些操作完全是不必要的,所以在點分樹上修改就可以做到一個 \(\log n\) 複雜度
所以這些維護資訊的資料結構往往還需要支援修改
如這題,就可以用樹狀陣列來維護修改維護字首和了。
至此,點分樹介紹以及本題題解完全結束。
程式碼中附註釋。
#include<bits/stdc++.h>
using namespace std;
const int inf=(1<<30);
const int N=2e5+10;
int n,m,tot,ans,rt,sum,minn,cnt,head[N];
inline int Min(int x,int y){return x<y?x:y;}
inline int Max(int x,int y){return x>y?x:y;}
int val[N],siz[N],dep[N],pa[N],pos[N],lg[N],st[N][21];
bool vis[N];
vector<int>C[2][N];
struct E{int nxt,to;}e[N<<1];
inline void add(int x,int y){e[++tot]=(E){head[x],y};head[x]=tot;}
void dfs1(int x,int fa){
st[++cnt][0]=x;pos[x]=cnt;dep[x]=dep[fa]+1;
for(int i=head[x];i;i=e[i].nxt){
int j=e[i].to;
if(j==fa)continue;
dfs1(j,x);st[++cnt][0]=x;//原樹的尤拉序
}
}
inline int getmin(int x,int y){return dep[x]<dep[y]?x:y;}
void GetEuler(){
for(int i=1;i<=cnt;++i)lg[i]=31-__builtin_clz(i);
for(int t=1;(1<<t)<=cnt;++t){
for(int i=1;i+(1<<t)<=cnt;++i){
st[i][t]=getmin(st[i][t-1],st[i+(1<<(t-1))][t-1]);//維護關於尤拉序的st表
}
}
}
inline int getdis(int u,int v){
if(pos[u]>pos[v])swap(u,v);
int pu=pos[u],pv=pos[v],len=pos[v]-pos[u]+1;
//求兩點第一次出現尤拉序中深度最小的節點
int L=getmin(st[pu][lg[len]],st[pv-(1<<lg[len])+1][lg[len]]);
return dep[u]+dep[v]-(dep[L]<<1);//獲得原樹中兩點距離
}
inline int lowbit(int x){return x&(-x);}
void change(int u,int opt,int x,int v){
x++;//注意樹狀陣列下標不能是0
for(int i=x;i<=siz[u];i+=lowbit(i))C[opt][u][i]+=v;
}
int query(int u,int opt,int x){
x++;
int res=0;
x=Min(x,siz[u]);
for(int i=x;i;i-=lowbit(i))res+=C[opt][u][i];
return res;
}
void Gr(int x,int fa){
siz[x]=1;
int res=0;
for(int i=head[x];i;i=e[i].nxt){
int j=e[i].to;
if(j==fa||vis[j])continue;//避免走父親以及之前的重心
Gr(j,x);siz[x]+=siz[j];res=Max(res,siz[j]);
}
res=Max(res,sum-siz[x]);
if(res<minn)minn=res,rt=x;//從子樹裡面找重心
}
void dfs(int u){
vis[u]=1;siz[u]=sum+1;//vis表示是不是已經在點分樹中
C[0][u].resize(siz[u]+1);
C[1][u].resize(siz[u]+1); //提前定好其大小,最長就是鏈
for(int i=head[u];i;i=e[i].nxt){
int j=e[i].to;
if(vis[j])continue;
sum=siz[j];rt=0;minn=inf;
Gr(j,0);pa[rt]=u;dfs(rt);//找j的重心並與當前點分樹點連邊
}
}
void modify(int u,int w){
for(int i=u;i;i=pa[i])change(i,0,getdis(u,i),w);//把子樹內對x的貢獻算上
for(int i=u;pa[i];i=pa[i])change(i,1,getdis(u,pa[i]),w);//計運算元樹內點對fa的貢獻
}
int opt;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)scanf("%d",&val[i]);
for(int i=1;i<n;++i){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
dfs1(1,0);GetEuler();sum=n;minn=inf;
Gr(1,0);dfs(rt);//找到 1 的重心,然後建立點分樹
for(int i=1;i<=n;++i)modify(i,val[i]);//初始化,dis啥的在modify裡
for(;m;m--){
int x,y;
scanf("%d%d%d",&opt,&x,&y);
x^=ans;y^=ans;
if(!opt){
ans=query(x,0,y);//x子樹內的
for(int i=x;pa[i];i=pa[i]){
int dis=getdis(x,pa[i]);
if(y>=dis)ans+=query(pa[i],0,y-dis)-query(i,1,y-dis);//父節點子樹中x子樹外的
}
printf("%d\n",ans);
}
else modify(x,y-val[x]),val[x]=y;//單點修改
}//點分樹的結構與原樹的結構關係不大 所以距離什麼的要放到第一次dfs裡面處理出來
return 0;
}