浅谈树分治相关算法

被迫营业。

对于树上的路径统计问题,我们一直存在一个比较暴力的做法,就是从根节点出发,先统计所有以根为 ( ext{lca}) 的路径,然后依次递归每一棵子树,同时使用上面的方法。这个方法的复杂度显然是 (O(n^2)) 的,我们可以轻易的用一条链卡掉,于是问题来了,如果题目让我们统计所有的路径的某个信息,我们该如何统计呢?

我们观察上面的算法,我们可以发现一个点被一个计算到的次数是与其深度有关的,所以我们才能轻易地用一条链来卡掉这个算法。所以我们能否通过改变遍历顺序的方式来降低“树高”呢?答案是可以的。

点分治

点分治是一种暴力统计树上所有路径信息的方法。

我们每次对于当前子树,用重心来作为我们上面算法的树根来操作,同时在每一个该子树的子树中递归执行这个算法。发现树高(在这里就是递归深度)就被我们轻易的降到了 (log_2n) 的级别,成功使得上面的暴力正确了。

边分治

这个东西其实和点分治差不多,点分治是找最中间的一个点,边分治就是找最中间的一个边,将过这个边的路径都统计完了之后分成两个子树。

然后你发现这个东西被菊花图随便卡。

然后你需要处理一下原树将其变成一棵二叉树然后就可以了。

点分树

这个东西非常简单,就是把你点分治过程中 ( ext{dfs}) 的顺序变成一棵树就行了。

边分树

基本同上。

Tricks of 点分树

Trick1

由于我们是在点分树上每一个节点统计子树内部到该点的答案和,且我们在统计答案的时候是向上跳的,所以我们必须要做一点容斥,容斥的搞法要视题目而定,一般的做法是另统计该点子树到该点父亲的答案和,然后减去即可。

这是一个很简单的很常用的技巧,与其说是技巧,不如说是基本必备的部分。

例题1:P6329 【模板】点分树 | 震波

很板子的一道题目,建出点分树后按照 trick1 直接维护即可。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,lstans=0,a[N];
struct Seg_Tree{
	int rt[N],tot;
	struct Node{int ls,rs,data;}tr[N<<6];
	void add(int u,int l,int r,int x,int z){
		tr[u].data+=z;if(l==r) return ;
		int mid=(l+r)>>1;
		if(x<=mid){
			if(!tr[u].ls) tr[u].ls=++tot;
			return add(tr[u].ls,l,mid,x,z);
		}
		else{
			if(!tr[u].rs) tr[u].rs=++tot;
			return add(tr[u].rs,mid+1,r,x,z);
		}
	}
	int query(int u,int l,int r,int x,int y){
		if(x>y) return 0;
		if(x<=l&&r<=y) return tr[u].data;
		int mid=(l+r)>>1,res=0;
		if(x<=mid) res+=query(tr[u].ls,l,mid,x,y);
		if(y>mid) res+=query(tr[u].rs,mid+1,r,x,y);
		return res;
	}
}t3,t4;
struct Tree{
	struct Node{int size,dep,fa[20];}tr[N];
	struct Edge{int nxt,to;bool alive;}e[N<<1];int fir[N];
	void add(int u,int v,int i){e[i]=(Edge){fir[u],v,true},fir[u]=i;}
	void dfs(int u){
		for(int i=fir[u];i;i=e[i].nxt){
			if(e[i].to==tr[u].fa[0]) continue;
			tr[e[i].to].dep=tr[u].dep+1;
			tr[e[i].to].fa[0]=u,dfs(e[i].to);
		}
	}
	void init(){
		for(int j=1;j<20;++j){
			for(int i=1;i<=n;++i)
			tr[i].fa[j]=tr[tr[i].fa[j-1]].fa[j-1];
		}
	}
	int lca(int u,int v){
		if(tr[u].dep<tr[v].dep) swap(u,v);
		for(int j=19;j>=0;--j){
			if(tr[tr[u].fa[j]].dep>=tr[v].dep)
			u=tr[u].fa[j];
		}
		if(u==v) return u;
		for(int j=19;j>=0;--j){
			if(tr[u].fa[j]!=tr[v].fa[j])
			u=tr[u].fa[j],v=tr[v].fa[j];
		}
		return tr[u].fa[0];
	}
	int dis(int u,int v){
		int tmp=lca(u,v);
		return tr[u].dep+tr[v].dep-2*tr[tmp].dep;
	}
}t1;
struct Tree_with_dianfen:public Tree{
	int rt,cnt_edge;Tree t;
	void find_root(int u,int fa,int size,int &rt){
		int tmp=0;t.tr[u].size=1;
		for(int i=t.fir[u];i;i=t.e[i].nxt){
			int v=t.e[i].to;
			if(v==fa||!t.e[i].alive) continue;
			find_root(v,u,size,rt);
			t.tr[u].size+=t.tr[v].size;
			tmp=max(tmp,t.tr[v].size);
		}
		tmp=max(tmp,size-t.tr[u].size);
		if(tmp<=size/2) rt=u;
	}
	int build(int u,int size){
		int rt=0;
		find_root(u,0,size,rt),tr[rt].size=size;
		for(int i=t.fir[rt];i;i=t.e[i].nxt){
			int v=t.e[i].to;
			if(!t.e[i].alive) continue;
			t.e[i].alive=t.e[i^1].alive=false;
			if(t.tr[v].size>t.tr[rt].size)
			add(rt,build(v,size-t.tr[rt].size),++cnt_edge);
			else add(rt,build(v,t.tr[v].size),++cnt_edge);
		}
		return rt;
	}
	void modify(int x,int y){
		int u=x;
		while(u){
			int fa=tr[u].fa[0];
			if(!t3.rt[u]) t3.rt[u]=++t3.tot;
			t3.add(t3.rt[u],0,tr[u].size,t.dis(u,x),y);
			if(!fa) break;
			if(!t4.rt[u]) t4.rt[u]=++t4.tot;
			t4.add(t4.rt[u],0,tr[fa].size,t.dis(fa,x),y);
			u=fa;
		}
	}
	int query(int x,int y){
		int u=x,v=0,res=0;
		while(u){
			res+=t3.query(t3.rt[u],0,tr[u].size,0,y-t.dis(u,x));
			res-=t4.query(t4.rt[v],0,tr[u].size,0,y-t.dis(u,x));
			v=u,u=tr[u].fa[0];
		}
		return res;
	}
}t2;
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	for(int i=1;i<n;++i){
		int u,v;scanf("%d%d",&u,&v);
		t1.add(u,v,i<<1),t1.add(v,u,i<<1|1);
	}
	t1.tr[1].dep=1,t1.dfs(1),t1.init();
	t2.t=t1,t2.rt=t2.build(1,n),t2.dfs(t2.rt);
	for(int i=1;i<=n;++i) t2.modify(i,a[i]);
	for(int i=1;i<=m;++i){
		int opt,x,y;
		scanf("%d%d%d",&opt,&x,&y);
		x^=lstans,y^=lstans;
		if(opt) t2.modify(x,y-a[x]),a[x]=y;
		else printf("%d
",lstans=t2.query(x,y));
	}
	return 0;
}

Trick2

可以证明,如果你在点分树中的每一个点中直接存下子树内部所有点到该点的答案,空间复杂度是对的。(这个 trick 很显然但我觉得很容易忘,故写之。)

例题2:P3241 [HNOI2015]开店

前两个 tricks 结合一下就可以做了。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,q,m;
long long lstans=0;
struct Tree{
	struct Node{int dep,dis,fa[20];}tr[N];
	struct Edge{int nxt,to,val;bool alive;}e[N<<1];int fir[N];
	void add(int u,int v,int w,int i){e[i]=(Edge){fir[u],v,w,true},fir[u]=i;}
	void dfs(int u){
		for(int i=fir[u];i;i=e[i].nxt){
			if(e[i].to==tr[u].fa[0]) continue;
			tr[e[i].to].dep=tr[u].dep+1;
			tr[e[i].to].dis=tr[u].dis+e[i].val;
			tr[e[i].to].fa[0]=u,dfs(e[i].to);
		}
	}
	void init(){
		for(int j=1;j<=19;++j){
			for(int i=1;i<=n;++i)
			tr[i].fa[j]=tr[tr[i].fa[j-1]].fa[j-1];
		}
	}
	int lca(int u,int v){
		if(tr[u].dep<tr[v].dep) swap(u,v);
		for(int j=19;j>=0;--j){
			if(tr[tr[u].fa[j]].dep>=tr[v].dep)
			u=tr[u].fa[j];
		}
		if(u==v) return u;
		for(int j=19;j>=0;--j){
			if(tr[u].fa[j]!=tr[v].fa[j])
			u=tr[u].fa[j],v=tr[v].fa[j];
		}
		return tr[u].fa[0];
	}
	int dis(int u,int v){
		int tmp=lca(u,v);
		return tr[u].dis+tr[v].dis-2*tr[tmp].dis;
	}
}t1;
struct Dianfen_Tree{
	Tree *t;int rt,cnt_edge;
	struct Node{int data,size,fa;vector<pair<int,long long> > bag1,bag2;}tr[N];
	struct Edge{int nxt,to;}e[N<<1];int fir[N];
	void add(int u,int v,int i){e[i]=(Edge){fir[u],v},fir[u]=i;}
	void dfs(int u,int fa,int size,int &rt){
		tr[u].size=1;int tmp=0;
		for(int i=t->fir[u];i;i=t->e[i].nxt){
			int v=t->e[i].to;
			if(v==fa||!t->e[i].alive) continue;
			dfs(v,u,size,rt),tr[u].size+=tr[v].size;
			tmp=max(tmp,tr[v].size);
		}
		tmp=max(tmp,size-tr[u].size);
		if(tmp<=size/2) rt=u;
	}
	int build(int u,int size){
		int rt=0;dfs(u,0,size,rt);
		for(int i=t->fir[rt];i;i=t->e[i].nxt){
			int v=t->e[i].to,tmp;
			if(!t->e[i].alive) continue;
			t->e[i].alive=t->e[i^1].alive=false;
			if(tr[v].size<=tr[rt].size) tmp=build(v,tr[v].size);
			else tmp=build(v,size-tr[rt].size);
			add(rt,tmp,++cnt_edge),tr[tmp].fa=rt;
		}
		return tr[rt].size=size,rt;
	}
	void insert(int x,int y){
		int u=x,v=0;
		while(u){
			tr[u].bag1.push_back(make_pair(y,t->dis(u,x)));
			if(v) tr[v].bag2.push_back(make_pair(y,t->dis(u,x)));
			v=u,u=tr[u].fa;
		}
	}
	long long query(int x,int l,int r){
		int u=x,v=0,L,R;long long res=0;
		while(u){
			L=lower_bound(tr[u].bag1.begin(),tr[u].bag1.end(),make_pair(l,0ll))-tr[u].bag1.begin();
			R=lower_bound(tr[u].bag1.begin(),tr[u].bag1.end(),make_pair(r+1,0ll))-tr[u].bag1.begin();
			res+=tr[u].bag1[R-1].second-tr[u].bag1[L-1].second+1ll*(R-L)*t->dis(u,x);
			if(v){
				L=lower_bound(tr[v].bag2.begin(),tr[v].bag2.end(),make_pair(l,0ll))-tr[v].bag2.begin();
				R=lower_bound(tr[v].bag2.begin(),tr[v].bag2.end(),make_pair(r+1,0ll))-tr[v].bag2.begin();
				res-=tr[v].bag2[R-1].second-tr[v].bag2[L-1].second+1ll*(R-L)*t->dis(u,x);
			}
			v=u,u=tr[u].fa;
		}
		return res;
	}
}t2;
signed main(){
	cin>>n>>q>>m;
	for(int i=1;i<=n;++i) scanf("%d",&t2.tr[i].data);
	for(int i=1;i<n;++i){
		int u,v,w;scanf("%d%d%d",&u,&v,&w);
		t1.add(u,v,w,i<<1),t1.add(v,u,w,i<<1|1);
	}
	t1.tr[1].dep=1,t1.dfs(1),t1.init();
	t2.t=&t1,t2.rt=t2.build(1,n);
	for(int i=1;i<=n;++i) t2.insert(i,t2.tr[i].data);
	for(int i=1;i<=n;++i){
		t2.tr[i].bag1.push_back(make_pair(-1,0ll));
		sort(t2.tr[i].bag1.begin(),t2.tr[i].bag1.end());
		for(int j=1;j<(int)t2.tr[i].bag1.size();++j)
		t2.tr[i].bag1[j].second+=t2.tr[i].bag1[j-1].second;
		t2.tr[i].bag2.push_back(make_pair(-1,0ll));
		sort(t2.tr[i].bag2.begin(),t2.tr[i].bag2.end());
		for(int j=1;j<(int)t2.tr[i].bag2.size();++j)
		t2.tr[i].bag2[j].second+=t2.tr[i].bag2[j-1].second;
	}
	for(int i=1;i<=q;++i){
		int u,l,r;scanf("%d%d%d",&u,&l,&r);
		l=(l+lstans)%m,r=(r+lstans)%m;if(l>r) swap(l,r);
		printf("%lld
",lstans=t2.query(u,l,r));
	}
	return 0;
}

Trick3

这个应该不算 trick ,但是挺有用,因为有一道题目好像只能这么过,就是:

例题3:P3920 [WC2014]紫荆花之恋

首先声明,这道题目我还没过(根本就没写),但是可以大概口胡一下。

发现点分树不能加点,看似这道题目好像搞不了,然后你就想到了替罪羊树。

你可以仿照替罪羊树的写法来重构点分树来保证复杂度,然后细节应该非常多。

代码

没有,要的话自己去网上找。

一句话总结

树分治算法实际上就是优雅一点的暴力。

原文地址:https://www.cnblogs.com/Point-King/p/14697825.html