Always ke|

zyxawa

园龄:2年4个月粉丝:6关注:7

树分治、动态树分治学习笔记

点分治

点分治适合处理大规模的树上路径信息问题,选取重心,将当前的树拆分为几颗子树,然后递归子树求解问题,但是今天的重点不在这里


边分治

与点分治类似,选取一条边,均匀地将树分成两个部分,但是对于一个点有多个儿子时,时间复杂度就会非常大,于是我们可以将其转化,这里有两种方法

1.对于一个点 x,如果它的节点数小于等于 2,那么直接向子节点连边即可;否则新建两个点,将 x 连向这两个点,边权赋值为 0,并将 x 的子节点按奇偶分类暂时归为这两个新建点的子节点,如果新建节点的子节点过多,可以继续新建,就像线段树一样

1

2.对于原树每个点 x,记录一个 last(初始为 x),每次将 last 连向一个子节点,并新建一个点 ylast 连向 y,然后将 last 改为 y,边权同为 0

2

对于边分治,我们有这些性质:递归深度为 log(n),最多增加 n 个节点,当然,如果我们要使用边分治,就必须要求新建的虚点和虚边对统计答案不会影响

例题:「BZOJ2870」最长道路

题意:给定一棵 n 个点的树,求树上一条链使得链上节点数量乘链上所有点中的最小权值所得的积最大

我们可以用边分治将树分为两个部分,dp1dp2 分别表示 u 到分治中心对应两边节点的距离与路径上点权 min

那么答案就是 ans=max(min(dp2x,dp2y)×(dp1x+dp2y+edgex,y+1))x,y 分别为选取边的两边的节点)

那么我们考虑把路径分为 dp2ydp2xdp2xdp2y 两类,然后用类似双指针的方式求出答案

示例代码:

#include<bits/stdc++.h>
using namespace std;
vector <int> G[50001];
int n,cnt=1,mx=1e9,rt,tot[2],ver[200001],vis[100001],head[100001],val[100001],edge[200001],nex[200001],siz[100001];
long long ans;
struct node{
	int dist,minn;
	bool operator<(node &a){
		return minn<a.minn;
	}
}a[2][100001];
void add(int x,int y,int z){
	ver[++cnt]=y;
	edge[cnt]=z;
	nex[cnt]=head[x];
	head[x]=cnt;
}
void build(int u,int f){
	for(int i=0,last=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v!=f){
			if(!last){
				add(u,v,1);
				add(v,u,1);
				last=u;
			}
			else if(i==G[u].size()-1){
				add(last,v,1);
				add(v,last,1);
			}
			else{
				val[++n]=val[u];
				add(last,n,0);
				add(n,last,0);
				add(n,v,1);
				add(v,n,1);
				last=n;
			}
		}
	}
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i];
		if(v!=f) build(v,u);
	}
}
void get(int u,int f,int all){
	siz[u]=1;
	for(int i=head[u];i!=0;i=nex[i]){
		int v=ver[i];
		if(!vis[i>>1]&&v!=f){
			get(v,u,all);
			siz[u]+=siz[v];
			if(max(siz[v],all-siz[v])<mx){
				mx=max(siz[v],all-siz[v]);
				rt=i;
			}
		}
	}
}
void dfs(int u,int f,int l,int m,int p){
	a[p][++tot[p]]={l,m};
	for(int i=head[u];i!=0;i=nex[i]){
		int v=ver[i];
		if(!vis[i>>1]&&v!=f) dfs(v,u,l+edge[i],min(m,val[v]),p);
	}
}
void solve(int u,int all){
	vis[u>>1]=1;
	tot[0]=tot[1]=0;
	dfs(ver[u],0,0,val[ver[u]],0);
	dfs(ver[u^1],0,0,val[ver[u^1]],1);
	sort(a[0]+1,a[0]+tot[0]+1);
	sort(a[1]+1,a[1]+tot[1]+1);
	for(int i=tot[0],p=tot[1]+1,lj=0;i>=1;i--){
		while(p>1&&a[1][p-1].minn>=a[0][i].minn){
			p--;
			lj=max(lj,a[1][p].dist);
		}
		if(p<=tot[1]) ans=max(ans,1ll*a[0][i].minn*(lj+a[0][i].dist+edge[rt]+1));
	}
	for(int i=tot[1],p=tot[0]+1,lj=0;i>=1;i--){
		while(p>1&&a[0][p-1].minn>=a[1][i].minn){
			p--;
			lj=max(lj,a[0][p].dist);
		}
		if(p<=tot[0]) ans=max(ans,1ll*a[1][i].minn*(lj+a[1][i].dist+edge[rt]+1));
	}
	int aus=siz[ver[u]];
	if(aus>1){
		mx=1e9;
		get(ver[u],0,aus);
		solve(rt,aus);
	} 
	aus=all-aus;
	if(aus>1){
		mx=1e9;
		get(ver[u^1],0,aus);
		solve(rt,aus);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&val[i]);
		ans=max(ans,1ll*val[i]);
	}
	for(int i=1,u=0,v=0;i<n;i++){
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
		G[v].push_back(u);
	}
	build(1,0);
	get(1,0,n);
	solve(rt,n);
	printf("%lld",ans);
	return 0;
}

点分树

就是将点分治递归的点提前递归求解出来,然后和它的上级中心连边建树即可,下面画图解释(图片来源 yxy 大佬,但是我好歹自己画了一遍

1.找到原树重心

1

2.重心将树划分为多个子树,递归查找子树重心,并连边

2

3.继续递归查找重心并连边

3

4.最后的鬼样子

4

当然这里必须介绍一些关于重心的性质,对做题有帮助:

1.树的重心如果不唯一,则至多有两个,且这两个重心相邻

2.一个点是重心,等价于以这个点为根,它的每个子树的大小都不会超过整个树大小的一半,所以点分树树高为 log2n

证明:利用反证法证明,假设 u 的一个子节点 vsizeu<sizev×2,所以 u 的其他节点的 size,即 (sizeusizev)×2<sizeu,那么很明显以 v 为根的树中的 max(size) 最大为 sizev1,那么选择 v 是更优于 u 的,冲突,得证。当然也有逆性质:一个点的每个子树的大小都不超过整个树大小的一半,就为重心

3.树中所有点到某个点的距离和中,到重心的距离和是最小的。如果有两个重心,那么到它们的距离和一样

证明:利用调试法,暴力跳动即可,设当前节点为 u,将跳动至 v,那么我们变化的值为 sizev(sizeusizev)<0 更优,即 2×sizev>sizeu,而重心就满足 2×sizev<sizeu,所以重心即为最优,更进一步同样具有逆定理,距离和最小与是重心等价

4.如果一个树增添,或删去一个叶子,则整个树的同一个重心最多移动一个节点

5.通过连接一条端点分别在两个树的边,来将两个树合并成一个,那么新的重心肯定是在原来这两个树的重心的路径上

当然这里只给出了第 23 个结论的证明,其他就感性理解一下即可,毕竟很明显(因为这两个最重要,因为我懒,自己看这个

当然:点分树的子树是原树的一个联通快,这也是在点分树上进行换根操作的基础

对于无修改的点分治的题目,点分树的意义其实不大,主要用来减少常数,就比如 「WC2010」重建计划


动态点分治

动态点分治用来解决带点权或边权修改的树上路径信息统计问题,对于修改其实就可以在点分树上暴力跳父亲节点修改,统计也差不多,但是具体由题目而定,一般来说对于一个节点需要记录它到点分树自己的子树的信息和它在点分树上的父亲节点到自己子树的信息,由于树高 log(n),所以时间复杂度得以保障,具体看例题

例题1「模板」点分树|震波

题意:查询操作:查询与 x 距离不超过 k 的节点的点券之和;修改操作:将 x 的点权改为 y

对于每个节点维护线段树(注意动态开点)distx 表示点分树上 x 子树到 x 的距离信息,下标为距离,权值为点权和,线段树 chx 表示 x 的子树到 x 在点分树上的父亲结点的距离信息

以查询操作为例,我们要先将答案加上线段树 distx 中下标从 0k 的权值和,然后我们遍历 x 的所有祖先 u,设其低一级祖先为 v(点分树上),令 d=disx,u(原树上),那么我们要将答案加上线段树 distu 中下标从 0kd 的权值和。由于我们重复计算了以 v 为根的部分,我们要将答案减去线段树 chv 中下标从 0kd 的权值和,修改操作没有重复,直接跳点分树上祖先直接修改 chxdistx 即可,修改方法类似

核心代码:

if(!op){
	last=dist.query(dist.root[x],0,n,0,y);
	for(int i=x;fa[i]!=0;i=fa[i]){
		last+=dist.query(dist.root[fa[i]],0,n,0,y-lca.dist(x,fa[i]));
		last-=ch.query(ch.root[i],0,n,0,y-lca.dist(x,fa[i]));
	}
	printf("%d\n",last);
}
else{
	dist.updata(dist.root[x],0,n,0,y-a[x]);
	for(int i=x;fa[i]!=0;i=fa[i]){
		dist.updata(dist.root[fa[i]],0,n,lca.dist(x,fa[i]),y-a[x]);
		ch.updata(ch.root[i],0,n,lca.dist(x,fa[i]),y-a[x]);
	}
	a[x]=y;
}
例题2「ZJOI2007」捉迷藏

题意:修改操作:改变第 i 个房间的照明状态,若原来打开,则关闭;若原来关闭,则打开,查询操作:查询最远的两个关灯房间的距离

操作类似,相对于上一题来说就是把线段树改为可删堆,再多维护一个 ans 记录全局最大值即可,甚至两个操作都类似,注意,需要堆的 size>1,一个点是无法构成路径的,当然 ans 需要插入一个 0

核心代码:

if(s[0]=='G'){
	if(all!=n) printf("%d\n",ans.top1());
	else printf("-1\n");
}
else{
	scanf("%d",&x);
	if(!col[x]){
		if(ch[x].size()>=2) ans.erase(ch[x].top1()+ch[x].top2());
		ch[x].erase(0);
		if(ch[x].size()>=2) ans.insert(ch[x].top1()+ch[x].top2());
		for(int i=x;fa[i]!=0;i=fa[i]){
			if(ch[fa[i]].size()>=2) ans.erase(ch[fa[i]].top1()+ch[fa[i]].top2());
			ch[fa[i]].erase(dist[i].top1());
			dist[i].erase(lca.dist(x,fa[i]));
			if(dist[i].size()) ch[fa[i]].insert(dist[i].top1());
			if(ch[fa[i]].size()>=2) ans.insert(ch[fa[i]].top1()+ch[fa[i]].top2());
		}
	}
	else{
		if(ch[x].size()>=2) ans.erase(ch[x].top1()+ch[x].top2());
		ch[x].insert(0);
		if(ch[x].size()>=2) ans.insert(ch[x].top1()+ch[x].top2());
		for(int i=x;fa[i]!=0;i=fa[i]){
			if(ch[fa[i]].size()>=2) ans.erase(ch[fa[i]].top1()+ch[fa[i]].top2());
			if(dist[i].size()) ch[fa[i]].erase(dist[i].top1());
			dist[i].insert(lca.dist(x,fa[i]));
			ch[fa[i]].insert(dist[i].top1());
			if(ch[fa[i]].size()>=2) ans.insert(ch[fa[i]].top1()+ch[fa[i]].top2());
		}
	}
	col[x]^=1;
	all+=(col[x]==1)-(col[x]==0);
}
例题3「ZJOI2015」幻想乡战略游戏

题意:给定一个带边权的树,初始点权为 0,给定 q 次操作,修改某一个节点的值,保证修改后为非负数,操作后回答:指定一个点 x,求最小的 val=i=1ndisx,i×wiwi 为点权)

方法1:线段树分治

cntuu 的子树的的点权和,all 为所有点的点权和,很明显这道题目是求一个带权重心,我们令最开始答案为 1,最优解不是 1,设当前节点为 u,要移动至 v,且它们的之间的边权为 d,那么 val 会减少 cntv×d,增加 (allcntv)×d,变化量为 (all2×cntv)×d,所以 2×cntv>all 时,解更优,很明显这样的点构成祖先后代关系,不然会大于 all,显然不成立

所以最终 val=i=1ndis1,i×wi+all×dis1,ans2×1>ansdi×cnti

求出 ans 节点我们可以线段树二分,由于点权、边权非负,所以我们满足 2×cntans>all 这个条件的节点肯定需要越深越好,并且我们的树链剖分节点的 dfn 越靠后那么它的深度也就更深,从而最优,所以我们尽量向右跳

示例代码:

#include<bits/stdc++.h>
using namespace std;
vector <pair<int,int> > G[100001];
long long all,full,dep[100001],size[100001];
int n,q,tot,fa[100001],son[100001],top[100001],dfn[100001],rnk[100001];
struct point{
	long long olds,news,lazy;
	int maxn;
}node[400001];
void dfs1(int rt,int from){
	size[rt]=1;
	son[rt]=-1;
	for(int i=0;i<G[rt].size();i++){
		int to=G[rt][i].second;
		if(to!=from){
			dep[to]=dep[rt]+G[rt][i].first;
			fa[to]=rt;
			dfs1(to,rt);
			size[rt]+=size[to];
			if(son[rt]==-1||size[to]>size[son[rt]]) son[rt]=to;
		}
	}
}
void dfs2(int rt,int t){
	top[rt]=t;
	dfn[rt]=++tot;
	rnk[tot]=rt;
	if(son[rt]==-1) return;
	dfs2(son[rt],t);
	for(int i=0;i<G[rt].size();i++){
		int to=G[rt][i].second;
		if(to!=fa[rt]&&to!=son[rt]) dfs2(to,to);
	}
}
void build(int rt,int l,int r){
	if(l==r){
		node[rt].olds=dep[rnk[l]]-dep[fa[rnk[l]]];
		return;
	}
	int mid=(l+r)/2;
	build(rt*2,l,mid);
	build(rt*2+1,mid+1,r);
	node[rt].olds=node[rt*2].olds+node[rt*2+1].olds;
}
void push_down(int rt){
	node[rt*2].news+=node[rt*2].olds*node[rt].lazy;
	node[rt*2+1].news+=node[rt*2+1].olds*node[rt].lazy;
	node[rt*2].maxn+=node[rt].lazy;
	node[rt*2+1].maxn+=node[rt].lazy;
	node[rt*2].lazy+=node[rt].lazy;
	node[rt*2+1].lazy+=node[rt].lazy;
	node[rt].lazy=0;
}
void updata(int rt,int l,int r,int L,int R,long long w){
	if(L<=l&&r<=R){
		node[rt].news+=node[rt].olds*w;
		node[rt].maxn+=w;
		node[rt].lazy+=w;
		return;
	}
	push_down(rt);
	int mid=(l+r)/2;
	if(L<=mid) updata(rt*2,l,mid,L,R,w);
	if(R>=mid+1) updata(rt*2+1,mid+1,r,L,R,w);
	node[rt].news=node[rt*2].news+node[rt*2+1].news;
	node[rt].maxn=max(node[rt*2].maxn,node[rt*2+1].maxn);
}
long long query(int rt,int l,int r,int L,int R){
	if(L<=l&&r<=R) return node[rt].news;
	push_down(rt);
	int mid=(l+r)/2;
	long long ret=0;
	if(L<=mid) ret+=query(rt*2,l,mid,L,R);
	if(R>=mid+1) ret+=query(rt*2+1,mid+1,r,L,R);
	return ret;
}
int find(int rt,int l,int r){
	if(l==r) return rnk[l];
	push_down(rt);
	int mid=(l+r)/2;
	if(2*node[rt*2+1].maxn>all) return find(rt*2+1,mid+1,r);
	else return find(rt*2,l,mid);
}
long long lookup(int x){
	long long ret=0;
	while(x){
		ret+=query(1,1,n,dfn[top[x]],dfn[x]);
		x=fa[top[x]];
	}
	return ret;
}
void modily(int x,long long e){
	while(x){
		updata(1,1,n,dfn[top[x]],dfn[x],e);
		x=fa[top[x]];
	}
}
int main(){
	scanf("%d%d",&n,&q);
	for(int i=1;i<n;i++){
		int l,r,w;
		scanf("%d%d%d",&l,&r,&w);
		G[l].push_back({w,r});
		G[r].push_back({w,l});
	}
	dep[1]=0;
	dfs1(1,0);
	dfs2(1,1);
	build(1,1,n);
	while(q--){
		int u,e;
		scanf("%d%d",&u,&e);
		modily(u,e);
		all+=e;
		full+=dep[u]*e;
		printf("%lld\n",full+all*dep[ans]-2*lookup(find(1,1,n)));
	}
	return 0;
}

方法2:动态点分治

这个就比线段树分治好理解多了,记录 sumvusumausumbu 分别为 u 的子树节点和, 子树内节点到达它的代价和(算上边权),子树内节点到达它的点分树上的父节点的代价和(算上边权)

对于如何求出答案,有一个暴力的思想,我们向重心移动一次相对不移动的代价是更优秀的,于是我们就可以在原树上暴力移动比较,由于最终只有一个节点满足最优,所以是肯定能找到答案的,但是就会 TLE,所以我们现在原树上当前节点的子节点只会有一个是更优秀的,所以我们找出它即可,但是如果是链,那么又没了,于是我们向当前节点在点分树上的子节点跳动,由于我们点分树的子树是原树的一个联通快,所以我们的大致方向是没有问题的,就继续比较,可以想象为二分

示例代码:

#include<bits/stdc++.h>
using namespace std;
vector <pair<int,int> > G[100001],T[100001];
struct dist{
	int cnt,fir[200001],lg[200001],dis[100001],st[400001][21];
	int lca(int u,int v){
		if(fir[u]>fir[v]) swap(u,v);
		int k=lg[fir[v]-fir[u]+1];
		return dis[u]+dis[v]-2*min(st[fir[u]][k],st[fir[v]-(1<<k)+1][k]);
	}
	void dfs(int u,int f){
		st[++cnt][0]=dis[u];
		fir[u]=cnt;
		for(int i=0;i<G[u].size();i++){
			int v=G[u][i].first;
			if(v!=f){
				dis[v]=dis[u]+G[u][i].second;
				dfs(v,u);
				st[++cnt][0]=dis[u];
			}
		}
	}
	void init(){
		lg[1]=dis[1]=cnt=0;
		memset(fir,0,sizeof(fir));
		for(int i=2;i<=200000;i++) lg[i]=lg[i/2]+1;
		dfs(1,0);
		for(int k=1;k<=lg[cnt];k++){
			for(int i=1;i+(1<<k)-1<=cnt;i++) st[i][k]=min(st[i][k-1],st[i+(1<<(k-1))][k-1]);
		}
	}
}d;
int n,q,x,y,w,rt=-1,root,dp[100001],siz[100001],fa[100001],vis[100001];
long long suma[100001],sumb[100001],sumv[100001];
void find(int u,int fa,int all){
	siz[u]=1;
	dp[u]=0;
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i].first;
		if(v!=fa&&!vis[v]){
			find(v,u,all);
			siz[u]+=siz[v];
			dp[u]=max(dp[u],siz[v]);
		}
	}
	dp[u]=max(dp[u],all-siz[u]);
	if(rt==-1||dp[u]<dp[rt]) rt=u;
}
void build(int u){
	vis[u]=1;
	for(int i=0;i<G[u].size();i++){
		int v=G[u][i].first;
		if(!vis[v]){
			rt=-1;
			find(v,u,siz[v]);
			T[u].push_back({rt,v});
			fa[rt]=u;
			build(rt);
		}
	}
}
void updata(int x,int y){
	sumv[x]+=y;
	for(int i=x;fa[i]!=0;i=fa[i]){
		int dis=d.lca(fa[i],x);
		sumv[fa[i]]+=y;
		suma[fa[i]]+=1ll*dis*y;
		sumb[i]+=1ll*dis*y;
	}
}
long long solve(int u){
	long long ans=suma[u];
	for(int i=u;fa[i]!=0;i=fa[i]){
		int dis=d.lca(fa[i],u);
		ans+=suma[fa[i]]-sumb[i]+dis*(sumv[fa[i]]-sumv[i]);
	}
	return ans;
}
long long query(int u){
	long long ans=solve(u);
	for(int i=0;i<T[u].size();i++){
		if(solve(T[u][i].second)<ans) return query(T[u][i].first);
	}
	return ans;
}
int main(){
	scanf("%d%d",&n,&q);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&x,&y,&w);
		G[x].push_back({y,w});
		G[y].push_back({x,w});
	}
	d.init();
	find(1,0,n);
	root=rt;
	build(root);
	while(q--){
		scanf("%d%d",&x,&y);
		updata(x,y);
		printf("%lld\n",query(root));
	}
	return 0;
}

本文作者:AKIOI

本文链接:https://www.cnblogs.com/zyxawa/p/18328039

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   zyxawa  阅读(5)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起