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

  1. \(u\) 未根节点

  2. \(u\) 有至少两个子树中存在 boundary

  3. 栈中剩余边数量大于 \(B\)

好,假若点 \(u\) 成为了 boundary node 接下考虑如何把其子树划进不同的 cluster

我们贪心的在栈中选择极长前缀直至以下情况发生

  1. 子树用完

  2. 新加入一个子树会使当前 cluster 中 boundary 数量超过 \(2\)

  3. 新加入一个子树会使当前 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)\)

posted @ 2024-01-30 23:57  ChiFAN鸭  阅读(196)  评论(0编辑  收藏  举报