树上分治算法 + 路径剖分

本文详细介绍了树上算法的三种方法:点分治、路径剖分和边分治。点分治用于解决与路径无关的问题,路径剖分将树上路径转化为链的维护,而边分治则关注于树的边。通过实例和代码展示了这些算法在解决具体问题中的应用,如统计距离、最长路径和路径上黑点数量等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树上分治算法 + 路径剖分

感谢qzc大佬的论文
上个学期就听说了点分治,但是一直没学过,看了da lao的论文后,醍醐灌顶。
我们熟悉的分治 是 线性的分治(参考归并排序)。树上分治就是把一个大的关于路径的问题,变成一个个小问题组成,大问题的答案可以由小问题的答案合并而来。

点分治

POJ 1741
点分治模板题,统计树上点对间距离小于等于K的对数。

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long

int n,k;
struct node{
	int to,w;
};
vector<node>v[10050];
int cnt=0;
int ans=0,root,tot;
int siz[10040],mx[10040],vis[10040];
int st[10040];
void getroot(int x,int pre){
	siz[x]=1;mx[x]=0;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||vis[to])continue;
		getroot(to,x);
		siz[x]+=siz[to];
		mx[x]=max(mx[x],siz[to]);
	}
	mx[x]=max(mx[x],tot-siz[x]);
	if(mx[x]<mx[root])root=x;
}
void getdis(int x,int pre,int dis){
	st[++cnt]=dis;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||vis[to])continue;
		getdis(to,x,dis+v[x][i].w);
	}
}
void getans(int x,int dis,int w){
	for(int i=1;i<=cnt;i++)st[i]=0;
	cnt=0;
	getdis(x,0,dis);
	sort(st+1,st+1+cnt);
	int stt=1,ed=cnt;
	while(stt<ed){
		if(st[stt]+st[ed]<=k){
			ans+=(ed-stt)*w;
			stt++;
		}else ed--; 
	} 
}
void divide(int x){
	vis[x]=1;
	getans(x,0,1);
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(vis[to])continue;
		getans(to,v[x][i].w,-1);
		root=0,tot=siz[to];
		getroot(to,0);
		divide(root);
	}
}
int main(){
	while(~scanf("%d%d",&n,&k)){
		if(!n&&!k)break;
		ans=0,cnt=0;
		for(int i=1;i<n;i++){
			int a,b,c;
			scanf("%d%d%d",&a,&b,&c);
			v[a].push_back({b,c});
			v[b].push_back({a,c});
			vis[i]=0;
		}
		vis[n]=0;
		root=0,mx[0]=1e9,tot=n;
		getroot(1,0);
		divide(root);
		printf("%d\n",ans);
		for(int i=1;i<=n;i++)v[i].clear();
	}
	return 0;
} 

SPOJ 1825

题意:
给你一颗树,每个节点不是黑点就是白点。求一条经过不超过K个黑点的最长路径的长度。

思路:
考虑点分治,对于每一层,我们只需要计算出经过当前重心的不超过K个黑点的距离。

首先我们考虑像树形DP的写法,我们每次只需要计算出当前子树,经过 i i i个黑点的最长长度,再和已经遍历过的子树去结合,得到不超过K个黑点的最长距离,当然,我们需要预处理出 前缀最大值,这样就能 O ( 子 树 链 上 最 大 黑 点 数 ) O(子树链上最大黑点数) O的求得当前子树的答案。
如果按照随意顺序的话,遍历一颗子树就是 m a x ( 当 前 子 树 链 上 最 大 黑 点 数 , 已 经 遍 历 过 的 子 树 链 上 最 大 黑 点 数 ) max(当前子树链上最大黑点数,已经遍历过的子树链上最大黑点数) max(,),这个复杂度是会到 O ( n 2 ) O(n^2) O(n2)级别的。
所以我们考虑启发式合并,我们从 子树链上最大黑点数 小的子树开始遍历,遍历完所有的子树,最多是 O ( n ) O(n) O(n)的。
这样复杂度就是 分治一个 l o g N logN logN,排序一个 l o g N logN logN,计算答案是 O ( N ) O(N) O(N)的,所以总的复杂度就是 O ( N l o g 2 N ) O(Nlog^2N) O(Nlog2N)的。

还有一种用树状数组的,就不用启发式合并了,之后补。

ops:
找遍全网没有找到一个用树形DP的写法的,所以就自己写了,调了很久。不用快读的话,过不过全看评测姬,我有一发1.06s过的,也有一发1.05s TLE 的。上了快读就平稳过了。

updated 2021-10-25:
找到了之前wa的原因,之前在分治的时候把合法儿子存下来,想着节省时间复杂度的,但是因为是递归算法+全局变量,所以在下一层会改变全局变量,然后回到这一层的时候,存的儿子已经是假的了,就wa了,哭哭……

启发式合并(树形DP)纯享版:

#include<bits/stdc++.h> 
using namespace std;
#define ll long long
 inline int qread(){
    int s=0,w=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
    for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
    return (w==-1?-s:s);}

int col[200050];
int n,k,m;
struct node{
	int to,w;
};
vector<node>v[200050];
int dep[200050];
int g[200050],f[200050];
int vis[200050],mx[200050],siz[200050];
int root,tot,ans=0,pre;
void getroot(int x,int pre){
	siz[x]=1,mx[x]=0;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||vis[to])continue;
		getroot(to,x);
		siz[x]+=siz[to];
		mx[x]=max(mx[x],siz[to]);
	}
	mx[x]=max(mx[x],tot-siz[x]);
	if(mx[x]<mx[root])root=x;
}
struct nod{
	int len,son,cnt;
}z[400050];
int cc=0;
void getcnt(int x,int pre,int pos,int cnt){
	cnt+=col[x];
	if(cnt>k)return;
	if(cnt>z[pos].cnt)z[pos].cnt=cnt;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||vis[to])continue;
		getcnt(to,x,pos,cnt);
	}
}
bool cmp(nod a,nod b){
	return a.cnt<b.cnt;
}
void getdis(int x,int pre,int dis,int cnt){
	cnt+=col[x];
	if(cnt>k)return;
	if(dis>f[cnt])f[cnt]=dis;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||vis[to])continue;
		getdis(to,x,dis+v[x][i].w,cnt);
	}
}
void divide(int x){
	vis[x]=1;
	int cc=0;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(vis[to])continue;
		cc++;
		z[cc].len=v[x][i].w;
		z[cc].son=to;z[cc].cnt=0;
		getcnt(to,0,cc,0);
	}
	sort(z+1,z+1+cc,cmp);
	for(int i=0;i<=z[cc].cnt;i++)g[i]=0;
	for(int i=1;i<=cc;i++){
		int to=z[i].son;
		for(int j=0;j<=z[i].cnt;j++)f[j]=-1e9;
		getdis(to,0,z[i].len,0);
		for(int j=1;j<=z[i].cnt;j++)f[j]=max(f[j-1],f[j]);
		for(int j=0;j<=z[i].cnt;j++){
			int id=min(z[i-1].cnt,k-j-col[x]);
			if(id<0)break;
			ans=max(ans,f[j]+g[id]);
		}
		for(int j=0;j<=z[i].cnt;j++)g[j]=max(g[j],f[j]);
		for(int j=1;j<=z[i].cnt;j++)g[j]=max(g[j-1],g[j]);
	}
	
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(vis[to])continue;
		root=0,tot=siz[to];
		getroot(to,0);
		divide(root);
	}
}
int main(){
	n=qread(),k=qread(),m=qread();
	for(int i=1;i<=m;i++){
		int a;a=qread();
		col[a]=1;
	}
	for(int i=1;i<n;i++){
		int a,b,c;
		a=qread(),b=qread(),c=qread();
		v[a].push_back({b,c});
		v[b].push_back({a,c});
	}
	tot=n,root=0,mx[0]=1e8;
	getroot(1,0);
	divide(root);
	printf("%d\n",ans);
	return 0;
} 

树状数组【舒适版】
时间比上面那份要多,常数较大,但是我过了。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

inline int qread(){
    ll s=0,w=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
    for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
    return (w==-1?-s:s);}

int col[200050];
int n,k,m;
struct node{
	int to,w;
};
vector<node>v[200050];
int dep[200050];
int vis[200050],mx[200050],siz[200050];
int root,tot,ans=0,pre;

int d[200005];
ll lowbit(ll i){
    return i&(-i);
}
void add(int i,int val){
	while(i<=n+1){
		d[i]=max(d[i],val);
		i+=lowbit(i);
	}
}
void del(int i){
    while(i<=n+1){
		d[i]=0;
		i+=lowbit(i);
    }
}
int query(int i){
	int ans=0;
	while(i){
		ans=max(ans,d[i]);
		i-=lowbit(i);
	}
	return ans;
}
void getroot(int x,int pre){
	siz[x]=1,mx[x]=0;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||vis[to])continue;
		getroot(to,x);
		siz[x]+=siz[to];
		mx[x]=max(mx[x],siz[to]);
	}
	mx[x]=max(mx[x],tot-siz[x]);
	if(mx[x]<mx[root])root=x;
}
int f[200050];
void getdis(int x,int pre,int dis,int cnt){
	f[cnt]=max(f[cnt],dis);
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||vis[to])continue;
		if(col[to])getdis(to,x,dis+v[x][i].w,cnt+1);
		else getdis(to,x,dis+v[x][i].w,cnt);
	}
}
void getdep(int x,int pre){
	dep[x]=0;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(vis[to]||to==pre)continue;
		getdep(to,x);
		dep[x]=max(dep[to],dep[x]);
	}
	if(col[x])dep[x]++;
}
void divide(int x){
	vis[x]=1;
	getdep(x,0);
	for(int i=0;i<=dep[x];i++)del(i+1);
	int pre=0;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(vis[to])continue;
		for(int j=0;j<=dep[to];j++)f[j]=-1e9;
		getdis(to,0,v[x][i].w,col[to]);
		for(int j=0;j<=dep[to];j++){
			int id=min(pre,k-j-col[x]);
			if(id<0)break;
			ans=max(ans,f[j]+query(id+1));
		}
		for(int j=0;j<=dep[to];j++)add(j+1,f[j]);
		pre=max(pre,dep[to]);
	}
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(vis[to])continue;
		root=0;tot=siz[to];
		getroot(to,0);
		divide(root);
	}
}
int main(){
	n=qread();k=qread();m=qread();
	for(int i=1;i<=m;i++){
		int a;a=qread();
		col[a]=1;
	}
	for(int i=1;i<n;i++){
		int a,b,c;
		a=qread();b=qread();c=qread();
		v[a].push_back({b,c});
		v[b].push_back({a,c});
	}
	dep[0]=0;
	tot=n,root=0,mx[0]=1e8;
	getroot(1,0);
	divide(root);
	printf("%d\n",ans);
	return 0;
}

路径剖分

树链剖分之后就可以把树上一条路径分成 l o g log log个连续的区间,就可以搭配线段树等数据结构啦。
当然也可以支持一些比较复杂的跳祖先的操作,这样能比较简单地维护一些子树信息。

SPOJ 375
一道模板树链剖分题,试试手吧
题意:
给你一颗有边权的树,你需要支持两种操作:
1、把第 i i i条边的权值改成 t i ti ti
2、询问点 a a a到点 b b b的简单路径上的最大边权
思路:
首先,肯定是边权转为点权,选择把边权转化成深度较大的点的点权 ,然后询问路径就靠树剖+线段树的 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)来好啦。
需要注意的点就是,建树以及询问的时候记得分清楚是原点还是 dfs序的点;以及,询问路径的时候,注意lca那个点的点权代表的 不是路径上的点,记得要跳过。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

int t,n;
struct node{
	int to,w;
};

struct nod{
	int a,b;
}z[10050];

vector<node>v[10050];
int dis[10050];
int cnt=0;
int siz[10050],dep[10050],son[10050],top[10050],id[10050],dfn[10050],fa[10050];
int tr[40060];
void dfs1(int x,int pre){
	fa[x]=pre;siz[x]=1;dep[x]=dep[pre]+1;son[x]=0;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre)continue;
		dis[v[x][i].to]=v[x][i].w;
		dfs1(to,x);
		siz[x]+=siz[to];
		if(siz[to]>siz[son[x]])son[x]=to;
	}
}
void dfs2(int x,int pre){
	if(son[pre]==x)top[x]=top[pre];
	else top[x]=x;
	id[x]=++cnt;
	dfn[cnt]=x;
	if(son[x]!=0)dfs2(son[x],x);
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||to==son[x])continue;
		dfs2(to,x);
	}
}
void build(int p,int l,int r){
	if(l==r){
		tr[p]=dis[dfn[l]];
		return ;
	}
	int mid=l+r>>1;
	build(2*p,l,mid);
	build(2*p+1,mid+1,r);
	tr[p]=max(tr[2*p],tr[2*p+1]);
}
void update(int p,int l,int r,int x,int w){
	if(l==r){
		tr[p]=w;
		return ;
	}
	int mid=l+r>>1;
	if(x<=mid)update(2*p,l,mid,x,w);
	else update(2*p+1,mid+1,r,x,w);
	tr[p]=max(tr[2*p],tr[2*p+1]);
}
int query(int p,int l,int r,int x,int y){
	if(x<=l&&r<=y){
		return tr[p];
	}
	int mid=l+r>>1;
	int ans=0;
	if(x<=mid)ans=max(ans,query(2*p,l,mid,x,y));
	if(mid<y)ans=max(ans,query(2*p+1,mid+1,r,x,y));
	return ans;
}
int sum(int x,int y){
	int ans=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]>dep[top[y]]){
			ans=max(ans,query(1,1,n,id[top[x]],id[x]));
			x=fa[top[x]];
		}else{
			ans=max(ans,query(1,1,n,id[top[y]],id[y]));
			y=fa[top[y]];
		}
	}
	if(x==y)return ans;
	if(dep[x]>dep[y])ans=max(ans,query(1,1,n,id[y]+1,id[x]));
	else ans=max(ans,query(1,1,n,id[x]+1,id[y]));
	return ans;
}
char s[10];
int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);
		for(int i=1;i<n;i++){
			int a,b,c;
			scanf("%d%d%d",&a,&b,&c);
			v[a].push_back({b,c});
			v[b].push_back({a,c});
			z[i].a=a;z[i].b=b;
		}
		cnt=0;
		dfs1(1,0);dfs2(1,0);
		build(1,1,n);
		while(1){
			scanf("%s",s);
			if(s[0]=='Q'){
				int a,b;
				scanf("%d%d",&a,&b);
				printf("%d\n",sum(a,b));
			}else if(s[0]=='C'){
				int a,b;
				scanf("%d%d",&a,&b);
				int x;
				if(dep[z[a].a]<dep[z[a].b])x=z[a].b;
				else x=z[a].a;
				update(1,1,n,id[x],b);
			}else{
				break;
			}
		}
		for(int i=1;i<=n;i++)v[i].clear();
	}
	return 0;
}

黑暗爆炸 3319
题意:
给你一颗边有黑白两种颜色的树,初始所有边都是白色,要求两种操作:
1、把从点a到点b的路径上的边都变成黑色
2、查询从u到根节点的一条黑边的编号。
思路:
树链剖分+线段树维护区间赋值,查询操作就通过跳链接近树,如果一条链的中有黑边,我们再去二分这条链即可。

代码:
Emmm……
这份代码是过不了黑暗爆炸上的那道题的,但是前七个样例都过了,死在了超时上。
复杂度是 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)的,这道题就当做是 用树剖进行爬祖先操作的示例吧。(过的人基本都是靠并查集维护的,因为他只有从白变成黑,所有变白之后直接用并查集 合并一下就好了)。

#include<bits/stdc++.h>
using namespace std;
#define ll long long

namespace fastIO
{
    static char buf[100000],*h=buf,*d=buf;//缓存开大可减少读入时间,看题目给的空间
    #define gc h==d&&(d=(h=buf)+fread(buf,1,100000,stdin),h==d)?EOF:*h++//不能用fread则换成getchar
    template<typename T>
    inline void read(T&x)
    {
        int f = 1;x = 0;
        char c(gc);
        while(c>'9'||c<'0'){
            if(c == '-') f = -1;
            c=gc;
        }
        while(c<='9'&&c>='0')x=(x<<1)+(x<<3)+(c^48),c=gc;
        x *= f;
    }
    template<typename T>
    void output(T x)
    {
        if(x<0){putchar('-');x=~(x-1);}
        static int s[20],top=0;
        while(x){s[++top]=x%10;x/=10;}
        if(!top)s[++top]=0;
        while(top)putchar(s[top--]+'0');
    }
}
using namespace fastIO;
vector<int>v[1000050];
struct node{
	int a,b;
}z[1000060];
int n,m,cnt=0;
int dep[1000050],siz[1000050],son[1000050],fa[1000050];
int top[1000050],id[1000050],dfn[1000050];
int t[4000050],laz[4000050];
int ans[1000050];
void dfs1(int x,int pre){
	siz[x]=1;son[x]=0;dep[x]=dep[pre]+1;fa[x]=pre;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i];
		if(to==pre)continue;
		dfs1(to,x);
		siz[x]+=siz[to];
		if(siz[to]>siz[son[x]])son[x]=to;
	}
}
void dfs2(int x,int pre){
	if(son[pre]==x)top[x]=top[pre];
	else top[x]=x;
	id[x]=++cnt;
	dfn[cnt]=x;
	if(son[x])dfs2(son[x],x);
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i];
		if(to==pre||to==son[x])continue;
		dfs2(to,x);
	}
}
void pushdown(int p,int l,int r){
	if(!laz[p])return;
	int mid=l+r>>1;
	if(t[2*p]!=mid-l+1){
		laz[2*p]=laz[p];t[2*p]=mid-l+1;
	}
	if(t[2*p+1]!=r-mid){
		laz[2*p+1]=laz[p];
		t[2*p+1]=r-mid;
	}
	
	laz[p]=0;
}
void update(int p,int l,int r,int x,int y){
	if(x>y)return;
	if(t[p]==r-l+1)return;
	if(x<=l&&r<=y){
		if(t[p]!=r-l+1){
			t[p]=r-l+1;
			laz[p]=1;
		}
		return;
	}
	int mid=l+r>>1;
	pushdown(p,l,r);
	if(x<=mid)update(2*p,l,mid,x,y);
	if(mid<y)update(2*p+1,mid+1,r,x,y);
	t[p]=t[2*p+1]+t[2*p];
}
int query(int p,int l,int r,int x,int y){
	if(x>y)return 0;
	if(x<=l&&r<=y)return t[p];
	int mid=l+r>>1;
	int ans=0;
	pushdown(p,l,r);
	if(x<=mid)ans+=query(2*p,l,mid,x,y);
	if(mid<y)ans+=query(2*p+1,mid+1,r,x,y);
	return ans;
}
int query2(int p,int l,int r,int k){
	if(l==r)return l;
	int mid=l+r>>1;
	pushdown(p,l,r);
	int w=t[2*p];
	if(w<k)return query2(2*p+1,mid+1,r,k-w);
	else return query2(2*p,l,mid,k);
}
void add(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]>dep[top[y]]){
			update(1,1,n,id[top[x]],id[x]);
			x=fa[top[x]];
		}else{
			update(1,1,n,id[top[y]],id[y]);
			y=fa[top[y]];
		}
	}
	if(x==y)return;
	if(dep[x]>dep[y])update(1,1,n,id[y]+1,id[x]);
	else update(1,1,n,id[x]+1,id[y]);
}
int sum(int x){
	while(x){
		int res=query(1,1,n,id[top[x]],id[x]);
		if(res!=0){
			int su=query(1,1,n,1,id[x]);
			int y=query2(1,1,n,su);
			return ans[dfn[y]];
		}
		x=fa[top[x]];
	}
	return 0;
}
int main(){
	read(n);read(m);
	for(int i=1;i<n;i++){
		read(z[i].a);read(z[i].b);
		v[z[i].a].push_back(z[i].b);
		v[z[i].b].push_back(z[i].a);
	}
	dep[0]=0;fa[0]=0;
	dfs1(1,0);dfs2(1,0);
	for(int i=1;i<n;i++){
		int x;
		if(dep[z[i].a]>dep[z[i].b])x=z[i].a;
		else x=z[i].b;
		ans[x]=i;
	}
	ans[1]=0;
	for(int i=1;i<=m;i++){
		int op;
		read(op);
		if(op==1){
			int a;read(a);
			output(sum(a));
			putchar('\n'); 
		}else{
			int a,b;
			read(a);read(b);
			add(a,b);
		}
	}
	return 0;
}

链分治

树链剖分 可以看成是一种树上 基于链的分治。
明白了这一点 就可以解决看上去和路径剖分无关的题目。

如何区分普通树链剖分 和 链分治呢,普通树链剖分是以点到根的路径作为维护对象,能较快地维护一条路径的信息;而链分治 是 通过分治的方法维护一个全局最优路径。

洛谷 P4115 / SPOJ 2666
题意:
给定一颗包含N个结点的树( N < = 100000 N<=100000 N<=100000),边权的绝对值小于等于1000,每个结点要么是黑色,要么是白色,初始都是白色。要求维护两种操作:
1、改变某个结点的颜色(白变黑,黑变白);
2、询问最远的两个白色结点之间的距离。

思路:
考虑如何计算路径的最高点在此条链中的最优值。
对于一条链,记此链的结点个数为 N, D ( i ) D(i) D(i) D 2 ( i ) D2(i) D2(i) 为此链的第 i i i个结点向下至某个白色结点的路径中长度的最大值和次大值。(两条路径仅在头结点(路径第一个结点)处相交,如果至白色结点的路径不存在,那么长度为 − I N F -INF INF )
我们的目标就是要求出满足与此链的重合部分在 [ 1 , N ] [1,N] [1,N] 的路径的最大长度。我们可以利用线段树来维护。
对于一个区间 [ L , R ] [L,R] [L,R] ,我们记录:
M a x L MaxL MaxL = m a x max max { d i s ( L , i ) + D ( i ) dis(L,i)+D(i) dis(L,i)+D(i) }
M a x R MaxR MaxR = m a x max max { D ( i ) + d i s ( i , R ) D(i)+dis(i,R) D(i)+dis(i,R) }
M a x V a l MaxVal MaxVal= 与此链的重合部分在 [ L , R ] [L,R] [L,R] 的路径的最大长度
d i s ( i , j ) dis(i,j) dis(i,j) 表示链上的第 i i i 个结点到第 j j j 个结点的距离。
设区间 [ L , R ] [L,R] [L,R] 的结点编号为 P P P L c Lc Lc R c Rc Rc 分别表示 P P P 的左右两个儿子,区间 [ L , m i d ] [L,mid] [L,mid] [ m i d + 1 , R ] [mid+1,R] [mid+1,R] ,我们可以得到如下转移:
M a x L ( P ) MaxL(P) MaxL(P) = M a x Max Max { M a x L ( L c ) , d i s ( L , m i d + 1 ) + M a x L ( R c ) MaxL(Lc), dis(L,mid+1) +MaxL(Rc) MaxL(Lc),dis(L,mid+1)+MaxL(Rc) }
M a x R ( P ) MaxR(P) MaxR(P) = M a x Max Max { M a x R ( R c ) , M a x R ( L c ) + d i s ( m i d , R ) MaxR(Rc), MaxR(Lc)+dis(mid,R) MaxR(Rc),MaxR(Lc)+dis(mid,R) }
M a x V a l ( P ) MaxVal(P) MaxVal(P)= M a x Max Max { M a x V a l ( L c ) , M a x V a l ( R c ) , M a x R ( L c ) + M a x L ( R c ) + d i s ( m i d , m i d + 1 ) MaxVal(Lc),MaxVal(Rc),MaxR(Lc)+MaxL(Rc)+dis(mid,mid+1) MaxVal(Lc),MaxVal(Rc),MaxR(Lc)+MaxL(Rc)+dis(mid,mid+1) }
这个转移很像最大字段和的转移
对于公式中的 d i s dis dis 我们可以用前缀和处理快速得到。
对于边界情况 [ L , L ] [L,L] [L,L] ,设此区间的结点编号为 P P P,此链的第 L L L 个结点为 x x x,那么
M a x L ( P ) MaxL(P) MaxL(P) = D ( L ) D(L) D(L)
M a x R ( P ) MaxR(P) MaxR(P) = D ( L ) D(L) D(L)
x x x 为白点时, M a x V a l ( P ) MaxVal(P) MaxVal(P) = M a x Max Max { D ( L ) , D ( L ) + D 2 ( L ) D(L),D(L)+D2(L) D(L),D(L)+D2(L) }
否则, M a x V a l ( P ) MaxVal(P) MaxVal(P) = D ( L ) + D 2 ( L ) D(L)+D2(L) D(L)+D2(L)
问题就只剩下如何维护 D D D D 2 D2 D2 的值
我们记 C 1... C k C1...Ck C1...Ck 表示 x x x k k k 个儿子(不包括同层结点(同层指的是 是否是一条重链)), L i Li Li 表示 C i Ci Ci 所在的链的线段树根节点, C o s t ( p ) Cost(p) Cost(p) 表示 ( x , p ) (x,p) (x,p) 的边权。那么点 x x x 向下至某个白色结点的路径长度集合为:
{ M a x L ( L i ) + C o s t ( C i ) , 0 MaxL(Li)+Cost(Ci),0 MaxL(Li)+Cost(Ci),0 } —— x x x为白点
{ M a x L ( L i ) + C o s t ( C i ) MaxL(Li)+Cost(Ci) MaxL(Li)+Cost(Ci) } —— x x x为黑点
我们可以用堆来维护这个集合,这样 D ( x ) D(x) D(x) D 2 ( x ) D2(x) D2(x) 的获取就是 O ( 1 ) O(1) O(1) 的了。
对于询问操作,我们可以用一个堆存贮每条链的最优值,这样我们就可以每次询问 O ( 1 ) O(1) O(1) 处理了。
对于修改操作,由于一个点只会影响 O ( l o g N ) O(log N) O(logN) 条链,每次都需要更改堆中的值和线段树,所以每次修改的时间复杂度为 O ( l o g 2 N ) O(log^2N) O(log2N)

代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int qread(){
    int s=0,w=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
    for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
    return (w==-1?-s:s);}

struct node{
	int to,w;
};
vector<node>v[100040];
int n,m,cnt=0,tot=0;
int fa[100050],siz[100050],son[100050],dis[100050];
int top[100050],id[100050],dfn[100050],len[100050],root[100050];
int ls[400050],rs[400050],lv[400050],rv[400050],mv[400050];
int w[100050];
struct nod{
	multiset<int,greater<int> >s;
	void push(int x){
		s.insert(x);
	}
	void pop(int x){
		auto it=s.lower_bound(x);
		if(it!=s.end())s.erase(it);
	}
	int top(){
		if(s.empty())return -1e8;
		else return *s.begin();
	}
}hp[100050],ans;
void dfs1(int x,int pre){
	siz[x]=1;fa[x]=pre;
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre)continue;
		dis[to]=dis[x]+v[x][i].w;
		dfs1(to,x);
		siz[x]+=siz[to];
		if(siz[to]>siz[son[x]])son[x]=to;
	}
}
void dfs2(int x,int pre){
	if(son[pre]==x)top[x]=top[pre];
	else top[x]=x;
	len[top[x]]++;
	id[x]=++cnt;
	dfn[cnt]=x;
	if(son[x])dfs2(son[x],x);
	for(int i=0;i<v[x].size();i++){
		int to=v[x][i].to;
		if(to==pre||to==son[x])continue;
		dfs2(to,x);
	}
}
void pushup(int p,int l,int r){
	int mid=l+r>>1;
	lv[p]=max(lv[ls[p]],lv[rs[p]]+dis[dfn[mid+1]]-dis[dfn[l]]);
	rv[p]=max(rv[rs[p]],rv[ls[p]]+dis[dfn[r]]-dis[dfn[mid]]);
	mv[p]=max(max(mv[ls[p]],mv[rs[p]]),dis[dfn[mid+1]]-dis[dfn[mid]]+rv[ls[p]]+lv[rs[p]]);
}
void build(int p,int l,int r){
	if(l==r){
		int x=dfn[l];
		for(int i=0;i<v[x].size();i++){
			int to=v[x][i].to;
			if(to==fa[x]||to==son[x])continue;
			hp[x].push(lv[root[to]]+v[x][i].w);
		}
		int d1=hp[x].top();
		hp[x].pop(d1);
		int d2=hp[x].top();
		hp[x].push(d1);
		lv[p]=rv[p]=max(0,d1);
		mv[p]=max(0,max(d1,d1+d2));
		return;	
	}
	int mid=l+r>>1;
	if(!ls[p])ls[p]=++tot;
	build(ls[p],l,mid);
	if(!rs[p])rs[p]=++tot;
	build(rs[p],mid+1,r);
	pushup(p,l,r);
}
void update(int p,int l,int r,int x,int tp){
	if(l==r){
		if(x!=tp)hp[x].push(lv[root[tp]]+dis[tp]-dis[x]);
		int d1=hp[x].top();
		hp[x].pop(d1);
		int d2=hp[x].top();
		hp[x].push(d1);
		if(w[x]){
			lv[p]=rv[p]=d1;
			mv[p]=d1+d2;
		}else{
			lv[p]=rv[p]=max(0,d1);
			mv[p]=max(0,max(d1,d1+d2));
		}
		return ;
	}
	int mid=l+r>>1;
	if(id[x]<=mid)update(ls[p],l,mid,x,tp);
	else update(rs[p],mid+1,r,x,tp);
	pushup(p,l,r);
}
void change(int x){
	int pre=x;
	while(x){
		int tp=top[x];
		int p1=mv[root[tp]];
		if(fa[tp])hp[fa[tp]].pop(lv[root[tp]]+dis[tp]-dis[fa[tp]]);
		update(root[tp],id[tp],id[tp]+len[tp]-1,x,pre);
		int p2=mv[root[tp]];
		if(p1!=p2){
			ans.pop(p1);
			ans.push(p2);
		}
		pre=tp;
		x=fa[tp];
	}
}
int main(){
	n=qread();
	for(int i=1;i<n;i++){
		int a,b,c;
		a=qread();b=qread();c=qread();
		v[a].push_back({b,c});
		v[b].push_back({a,c});
	}
	dfs1(1,0);dfs2(1,0);
	for(int i=n;i>=1;i--){
		int x=dfn[i];
		if(x==top[x]){
			if(!root[x])root[x]=++tot;
			build(root[x],id[x],id[x]+len[x]-1);
			ans.push(mv[root[x]]);
		}
	}
	m=qread();
	int num=n;
	while(m--){
		char s[10];
		scanf("%s",s);
		if(s[0]=='C'){
			int a;
			a=qread();
			w[a]^=1;
			if(w[a])num--;
			else num++;
			change(a);
		}else{
			if(!num)puts("They have disappeared.");
			else printf("%d\n",ans.top());
		}
	}
	return 0;
}

边分治

边分治是挑一条边就把树分成两颗子树,然后子树合并 相较点分治更加简单;
但是考虑菊花图,边分治会T,怎么办呢;
在 lzc 大佬的论文中,边分治的复杂度和 结点的最大度数有关,如果每个结点的度数是2或3,时间复杂度就是 n l o g n nlogn nlogn ,那么我可以把树转成一颗二叉树,这样最大度数就是 3;
但是用边分治,一定要保证改变树的结构不会影响答案。

重建树

void rebuild(int x,int pre){
	int tmp=0;
	int las=0;
	int len=v[x].size();
	for(int i=0;i<len;i++){
		int to=v[x][i].to;
		int val=v[x][i].w;
		if(to==pre)continue;
		tmp++;
		if(tmp==1){
			add(x,to,val);
			add(to,x,val);
			las=x;
		}else if(tmp==len-(x!=1)){
			add(las,to,val);
			add(to,las,val);
		}else{
			m++;
			add(las,m,0);add(m,las,0);
			las=m;
			add(m,to,val);add(to,m,val);
		}
	}
	for(int i=0;i<len;i++){
		int to=v[x][i].to;
		if(to==pre)continue;
		rebuild(to,x);
	}
}

还没敲过边分的题(实际是敲了一发 S P O J 1825 SPOJ 1825 SPOJ1825 wa了) 牢记新点不能对答案构成影响。

下次补……(其实边分能做的题,点分也能做,只不过比较麻烦 555~)
SPOJ 1825 / SPOJ 2666

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值