Top cluster 树分块
写点基础的东西。随便写的,勿喷。
top cluster
一个 cluster 是一个联通子图,且至多有两个点与其他部分连接
这两个点被称为 boundary node 其余点被称为 internal node,两个 boundary node 间的路径被称为 cluster path
而我们的树分块就是将原树分为 \(\sqrt n\) 个 cluster
将 boundary node 看成点,cluster 看成边,则构造出原图的收缩树
性质:不同的 cluster 最多共用两个 boundary node 而边集不交
构造方法
选取任意节点作为根,并强制根作为一个 boundary node
从根开始往下 dfs 并存储未归类进 cluster 的边(实际上存的是点,代表的是其连向其父亲的边)
当某个点 \(u\) 结束 dfs 假若发生以下三种情况之一则将其标记为 boundary node
-
\(u\) 未根节点
-
\(u\) 有至少两个子树中存在 boundary
-
栈中剩余边数量大于 \(B\)
好,假若点 \(u\) 成为了 boundary node 接下考虑如何把其子树划进不同的 cluster
我们贪心的在栈中选择极长前缀直至以下情况发生
-
子树用完
-
新加入一个子树会使当前 cluster 中 boundary 数量超过 \(2\)
-
新加入一个子树会使当前 cluster 大小超过 \(B\)
dfs 全部结束我们即可获得一种合法方案
根据论文里的结论,这样的划分 cluster 大小不超过 \(B\) 且数量不超过 \(6 \times \frac{n}{B}\)
贴一个板子题 P2420 (查询两点异或和)的代码,已经 AC
#include<bits/stdc++.h>
//#define int long long
//#define lowbit(x) (x&-(x))
using namespace std;
const int maxn = 1e5+114;
const int B = 318;
int fa[maxn];//原树上的父亲
int sz[maxn];//簇的大小
int tot;
int up[maxn],down[maxn];//所在的簇的上下界点
int dep[maxn];//深度
int st[maxn],top;//存贮还未分配的边
vector<int> edge[maxn];
int st_top[maxn];
int wait[maxn];//待分配的边
int low[maxn];//最浅界点
int CL[maxn],CLtot;
int CL_fa[maxn],CL_near[maxn],CL_up[maxn],CL_down[maxn];//在收缩树上的父亲 距离最近的簇路径上节点 所属簇的上下界点 (若为界点,则其所属簇为其作为下界点时所属的簇)
vector<int> cluster;//储存所有界点
vector<int> Down[maxn];//整个簇中出了上界点的点存入下界点中
int Point[maxn];//点权
int w[maxn];//一个簇上的上界点到根的路径上点权异或和
void work(int x){
for(int r=x;r!=0;r=fa[r]) w[x]^=Point[x];
}
void add_CL(int u,int v){//新建一个簇
if(!v) v=CL[CLtot];
CL_fa[v]=u,CL_near[u]=u;
for(int r=v;r!=u;r=fa[r])
CL_near[r]=r,w[v]^=Point[r];
for(int i=1;i<=CLtot;i++){
int r=CL[i],j;
CL_up[r]=u,CL_down[r]=v,Down[v].push_back(r);
for (j=r;!CL_near[j];j=fa[j]);
CL_near[r]=CL_near[j];
}
CLtot=0;
}
map<int,int> val[maxn];
void build(int u,int father){
dep[u]=dep[father]+1;
Point[u]=val[u][father];
fa[u]=father;
st_top[u]=top;
for(auto it=edge[u].begin();it!=edge[u].end();it++)
if((*it)==father){
edge[u].erase(it);
break;
}
wait[u]=true;
int cnt=0;
for(int v:edge[u]){
st[++top]=v;
build(v,u);
wait[u]+=wait[v];
low[v]&&(low[u]=low[v],cnt++);
}
if(wait[u]>B||cnt>1||father==0){
wait[u]=0,low[u]=u,cluster.push_back(u);
for(int i=0,j=st_top[u]+1,Cnt=0,cur_down=0,v;i<=edge[u].size();i++){
// Cnt 簇的大小 cur_down 簇的下界点
v=(i==edge[u].size())?0:edge[u][i];
if(Cnt+wait[v]>B||(cur_down&&low[v])||!v){ //已无法往当前簇中再加入一个子树
for (;(j<st_top[v]||!v)&&j<=top;j++)
CL[++CLtot]=st[j];
add_CL(u,cur_down),Cnt=cur_down=0;
}
Cnt+=wait[v],low[v]&&(cur_down=low[v]);
}
}
}
int n,q,rt;
int ask(int u){
int res=0;
while(u!=CL_up[u]) res^=Point[u],u=fa[u];
res^=w[u];
return res;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
val[u][v]=val[v][u]=w;
edge[u].push_back(v);
edge[v].push_back(u);
}
build(1,0);
for(int x:cluster) work(x);
cin>>q;
for(int i=1;i<=q;i++){
int u,v;
cin>>u>>v;
cout<<(ask(u)^ask(v))<<'\n';
}
return 0;
}
接下来讲点运用:
路径分解:
我们将点 \(u \to rt\) 的路径分为三段。
\(u \to v\) 其中 \(v\) 是 \(u\) 往上走遇到的第一个在 cluster path 上的节点
\(v \to w\) 其中 \(w\) 是 \(v\) 往上走遇到的第一个 up boundary node
\(w \to rt\) 其中 \(rt\) 是根节点
根据 top cluster 的性质,第一条和第二条路径长度不大于 \(B\)
而第三条路径可以在收缩树上解决,收缩树的大小为 \(O(\frac{n}{B})\)
而在这些地方维护前缀和或者差分就可以做到查询修改一者 \(O(\sqrt n)\) 另一者 \(O(1)\)。