树相关总结

**~~

树相关总结

**~~

LCA

倍增
struct edge{ int to,nxt; }e[MAX_E<<1];
int head[MAX_N],tote;
void add_edge(int u,int v){e[++tote].to=v,e[tote].nxt=head[u];head[u]=tote;}

int maxn,depth[MAX_N],dp[MAX_N][log2(MAX_N)+1];
void dfs(int u,int fa)
{
	dp[u][0]=fa,depth[u]=depth[fa]+1;
	repi(i,1,maxn) dp[u][i]=dp[dp[u][i-1]][i-1];
	reps(u)if(v!=fa) dfs(v,u);
}
int LCA(int x,int y)
{
	if(depth[x]<depth[y])	swap(x,y);
	repd(i,maxn,0)if((1<<i)<=(depth[x]-depth[y])) x=dp[x][i];
	if(x==y) return x;
	repd(i,maxn,0)if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i];
	return dp[x][0];
}
void init(int n)
{
	repi(i,0,n) head[i]=0;
	tote=0;
	maxn=floor(log(n+0.0)/log(2.0));
}
/*
depth[0]=0,dfs(rt,0);
*/
ST表
struct edge{ int to,nxt; }e[MAX_E<<1];
int head[MAX_N],tote;
void add_edge(int u,int v){e[++tote].to=v,e[tote].nxt=head[u];head[u]=tote;}
MAX_N)]],first[MAX_N];
int dp[MAX_N<<2][log2(MAX_N<<2)+1];
void init(int n)
{
	repi(i,0,n) head[i]=0;
	tot=tote=0;
}
void dfs(int u,int fa,int dep)
{
	ver[++tot]=u,first[u]=tot,depth[tot]=dep;
	reps(u)if(v!=fa) dfs(v,u,dep+1),ver[++tot]=u,depth[tot]=dep;
}
void ST(int n)
{
	repi(i,1,n) dp[i][0]=i;
	for(int j=1;(1<<j)<=n;j++){
		for(int i=1;i+(1<<j)-1<=n;i++){
			int a=dp[i][j-1],b=dp[i+(1<<(j-1))][j-1];
			dp[i][j]=depth[a]<depth[b]?a:b;
		}
	}
}	
int RMQ(int l,int r)
{
	int k=log2(r-l+1);
	return (depth[dp[l][k]]<depth[dp[r-(1<<k)+1][k]])?dp[l][k]:dp[r-(1<<k)+1][k];
}
int LCA(int x,int y)
{
	x=first[x],y=first[y];
	if(x>y) swap(x,y);
	return ver[RMQ(x,y)];
}
/*
dfs(rt,0,1),ST(tot);
*/

树的直径

性质

1.对于两棵树,如果第一棵树直径两端点为(u,v),第二棵树直径两端点为(x,y),用一条边将两棵树连接,那么新树的直径一定是u,v,x,y,中的两个点(不论是两棵树中哪两个点连接)
证明:如果新树直径不是原来两棵树中一棵的直径,那么新直径一定经过两棵树的连接边,新直径在原来每棵树中的部分一定是距离连接点最远的点,即一定是原树直径的一个端点。
2.对于一棵树,如果在一个点的上接一个叶子节点,那么最多会改变直径的一个端点
3.若一棵树存在多条直径,那么这些直径交于一点且交点是这些直径的中点(一条边/一个点 注意是按路径长而非边个数)

dfs(无法处理负权)
int st,ed,mx;
void dfs(int u,int fa,int dep)
{
	if(mx<dep) mx=dep,st=u;
	reps(u)if(v!=fa) dfs(v,u,dep+1);
}
/*	
mx=0,dfs(1,0,0);
ed=st;
mx=0,dfs(st,0,0);
*/
树形dp(可处理负权)
int ans,dp[MAX_N];//dp[i]以i为根的子树中到i距离最远的点 ans要初始化
void dfs_dp(int u,int fa)
{
	reps(u)if(v!=fa){
		dfs_dp(v,u);
		ans=max(ans,dp[u]+dp[v]+e[i].w);
		dp[u]=max(dp[u],dp[v]+e[i].w);
	}
}
// dfs_dp(1,0)

例题
1.P3304 求给定树有多少条边一定是直径边
解法1: 利用性质3.若一棵树存在多条直径,那么这些直径交于一点且交点是这些直径的中点(一条边/一个点 注意是按路径长而非边个数)。找到中心位置。
(1)若为一条边,则改变必选,另外就是边的两点。要看以这两点(拆掉中心边)为根的子树中,到达叶节点最远的路径中的必经边。记录ml数组,该点到以该点为根的子树叶结点的最远距离。递归处理,对当前节点,看可以通过几个节点到达最远,若为1,边必选,向下继续找,>1说明后续不存在必经边。
(2)若为一个点,找到该点连接的所有点,记录通过对应点能走的最远距离,取前3远的 fi se th,若一共就两个点/se>th,就对fi,se对应的点进行上面的递归处理,若fi>se se=th就单独对fi处理。其他情况说明不存在必经边。

int st,ed;
int pre[MAX_N];
ll mx,ml[MAX_N];
void dfs(int u,int fa,ll dep)
{
	pre[u]=fa,ml[u]=0;
	if(mx<dep) mx=dep,st=u;
	reps(u)if(v!=fa) dfs(v,u,dep+e[i].w),ml[u]=max(ml[u],ml[v]+e[i].w);
}
int ans=0;
void solve(int u,int fa)
{
	dfs(u,fa,0);
	while(true){
		ll mx=0,cnt=0,to;
		reps(u)if(v!=fa&&ml[v]+e[i].w>=mx){
			if(mx==ml[v]+e[i].w) cnt++;
			else mx=ml[v]+e[i].w,cnt=1,to=v;
		}
		if(cnt!=1) break;
		ans++,fa=u,u=to;	
	}
}
void init(int n)
{
	repi(i,0,n) head[i]=0;
	tote=0;
}
int main()
{
	int n; si(n); init(n);
	repi(i,1,n-1){
		int u,v; si(u),si(v);
		ll w; sl(w);
		add_edge(u,v,w),add_edge(v,u,w);
	}
	mx=0,dfs(1,0,0);
	ed=st;
	mx=0,dfs(st,0,0);
	printf("%lld\n",mx);
	int mid=st; 
	while(ml[pre[mid]]*2<=mx) mid=pre[mid];
	if(ml[mid]*2==mx){
		dfs(mid,0,0);
		int cnt=0;
		vector<P> rec;
		reps(mid) rec.pb(P(ml[v]+e[i].w,v));
		sort(all(rec));
		int len=rec.size();
		if(len==2||rec[len-2].fi>rec[len-3].fi) solve(rec[len-1].se,mid),solve(rec[len-2].se,mid);
		else if(rec[len-1].fi>rec[len-2].fi&&rec[len-2].fi==rec[len-3].fi) solve(rec[len-1].se,mid);
	}
	else{
		ans++;
		int a=mid,b=pre[mid];
		solve(a,b),solve(b,a);
	}
	printf("%d\n",ans);
	return 0;
}

解法2:对每条边u,v,求出以u,v为根的子树(断开这条边),求出以v为根子树到v向下,向上最远的距离及路径条数(两次树形dp)。若距离和积等于路径长度,路径之积等于直径路径数,则该边为必经边

struct node{
	ll w,cnt;//最远距离 边数
	node(){w=cnt=0;}
	node(ll _w,ll _cnt){ w=_w,cnt=_cnt; }
	void comb(ll _w,ll _cnt)
	{
		if(_w>w) w=_w,cnt=_cnt;
		else if(_w==w) cnt+=_cnt;
	} 
}dp1[MAX_N],dp2[MAX_N],mx;
ll w[MAX_N];
void dfs_1(int u,int fa)//处理出向下最远距离,路径数
{
	dp1[u].w=0,dp1[u].cnt=1;
	reps(u)if(v!=fa){
		w[v]=e[i].w,dfs_1(v,u);
		mx.comb(dp1[u].w+dp1[v].w+e[i].w,dp1[u].cnt*dp1[v].cnt);
		dp1[u].comb(dp1[v].w+e[i].w,dp1[v].cnt);
	}
}
int ans=0;
node front[MAX_N],rear[MAX_N];
void dfs_2(int u,int fa)//处理出向上的最远距离,路径数
{
	if(fa){
		dp2[u].w=dp2[u].cnt=0;
		dp2[u].comb(dp2[fa].w+w[u],dp2[fa].cnt);
		dp2[u].comb(front[u].w+w[u],front[u].cnt),dp2[u].comb(rear[u].w+w[u],rear[u].cnt);
		if(dp1[u].w+dp2[u].w==mx.w&&dp1[u].cnt*dp2[u].cnt==mx.cnt) ans++;
	} 
	vector<int> rec;
	node t;
	reps(u)if(v!=fa) rec.pb(v),front[v]=t,t.comb(dp1[v].w+e[i].w,dp1[v].cnt);		
	int len=rec.size();	t.w=t.cnt=0;
	repd(i,len-1,0) rear[rec[i]]=t,t.comb(dp1[rec[i]].w+w[rec[i]],dp1[rec[i]].cnt);
	reps(u)if(v!=fa) dfs_2(v,u);
}		
void init(int n)
{
	repi(i,0,n) head[i]=0;
	tote=0;
}
int main()
{
	int n; si(n); init(n);
	repi(i,1,n-1){
		int u,v; si(u),si(v);
		ll w; sl(w);
		add_edge(u,v,w),add_edge(v,u,w);
	}
	dfs_1(1,0),dp2[1].w=0,dp2[1].cnt=1,dfs_2(1,0);
	printf("%lld\n%d\n",mx.w,ans);
	return 0;
}

树的重心

性质

1.在一棵树中,所有点到某点的距离之和,是到重心的距离和最小.
2.把两棵树通过任何一点相连,形成的树的重心都在这两棵树的重心的连线上.
3.给一棵树添加或者删除一个节点,重心位置最多移动一格.
4.以重心为根,任何一棵子树的大小都不超过整棵树大小的一半.
5.删去重心及关联边,所有子树大小小于 ⌊ 总 点 数 2 ⌋ \left \lfloor \frac{总点数}{2} \right \rfloor 2
6. 设一棵树以x为根,那么它的重心要么是x,要么在x所在的重链上。

模板 求树的重心

int siz[MAX_N],w[MAX_N],mxsiz,rt,totsiz;//初始化mxsiz为inf,totsiz为树的节点个数
//int k,rt1,rt2;
void get_barycenter(int u,int fa)
{
	siz[u]=1,w[u]=0;
	reps(u)if(v!=fa){
		get_barycenter(v,u),siz[u]+=siz[v];
		w[u]=max(w[u],siz[v]);
	}
	w[u]=max(w[u],totsiz-siz[u]);
	if(mxsiz>w[u]) mxsiz=w[u],rt=u;
	/*
	if(w[u]
}

模板 求一颗树以任意一个节点为根的子树的重心

int siz[MAX_N],fa[MAX_N];
vector<int> ans[MAX_N];
void dfs(int u,int pre)
{
	fa[u]=pre,siz[u]=1;
	int son=-1;
	reps(u)if(v!=pre){
		dfs(v,u),siz[u]+=siz[v];
		if(son==-1||siz[v]>siz[son])	son=v;
	}
	if(son==-1)	ans[u].pb(u);
	else{
		son=ans[son][0];
		while((2*(siz[u]-siz[son]))>siz[u])	son=fa[son];
		ans[u].pb(son);
		if(siz[u]-siz[son]==siz[son])	ans[u].pb(fa[son]);
	}
}
/*
dfs(root,0)
*/

例题
1.P5666 求出一颗树删去每条边后,分裂出的两颗子树的重心编号和
设一棵树以x为根,那么它的重心要么是x,要么在x连出的重链上。
删去重心及关联边,所有子树大小小于 ⌊ 总 点 数 2 ⌋ \left \lfloor \frac{总点数}{2} \right \rfloor 2

利用这两个性质,对任何一颗树,要从根找到重心,只需一直沿着重链跳,每次跳的时候验证 节点数-以下一条节点为根的子树的节点数 小于等于节点数/2
最后停留的点及其重儿子,父节点都按性质判一下就可找到所有重心
首先倍增出每个节点向下跳的重儿子
枚举删除每条边,dfs处理
对每条边u-v,以v为根的子树直接倍增跳即可
对以u为根的,先找重儿子,再重新维护倍增的重链后跳着找即可

回溯时恢复原先维护的信息

int dp[MAX_N][19],pre[MAX_N],pre2[MAX_N];
int son[MAX_N][2],son2[MAX_N],siz[MAX_N],siz2[MAX_N];
void dfs(int u,int fa)
{
	siz[u]=1,pre[u]=fa;
	reps(u)if(v!=fa){
		dfs(v,u),siz[u]+=siz[v];
		if(siz[v]>siz[son[u][0]]) son[u][1]=son[u][0],son[u][0]=v;
		else if(siz[v]>siz[son[u][1]]) son[u][1]=v;
	}
	dp[u][0]=son[u][0];
	repi(i,1,18) dp[u][i]=dp[dp[u][i-1]][i-1];
}
ll ans;
ll check(int u,int sum)
{
	return 1ll*u*(max(siz2[son2[u]],sum-siz2[u])<=sum/2);
}
void dfs_2(int u,int fa)
{
	reps(u)if(v!=fa){//xxx2为新状态下的每个节点的相应数值

		siz2[u]=siz[1]-siz[v],pre2[u]=pre2[v]=0;
		son2[u]=(son[u][0]==v)?son[u][1]:son[u][0];
		if(siz2[fa]>siz2[son2[u]]) son2[u]=fa;

		dp[u][0]=son2[u];
		repi(j,1,18) dp[u][j]=dp[dp[u][j-1]][j-1];

		int tmp=u;
		repd(j,18,0)if(siz2[u]-siz2[dp[tmp][j]]<=siz2[u]/2) tmp=dp[tmp][j];
		ans+=check(son2[tmp],siz2[u])+check(tmp,siz2[u])+check(pre2[tmp],siz2[u]);

		tmp=v;
		repd(j,18,0)if(siz2[v]-siz2[dp[tmp][j]]<=siz2[v]/2) tmp=dp[tmp][j];
		ans+=check(son2[tmp],siz2[v])+check(tmp,siz2[v])+check(pre2[tmp],siz2[v]);

		pre2[u]=v,dfs_2(v,u);	
	}
	son2[u]=dp[u][0]=son[u][0],pre2[u]=pre[u],siz2[u]=siz[u];
	repi(i,1,18) dp[u][i]=dp[dp[u][i-1]][i-1];
}
void init(int n)
{
	repi(i,0,n) head[i]=son[i][0]=0;
	tote=ans=0;
} 
int main()
{
	int T; si(T);
	while(T--)
	{
		int n; si(n); init(n);
		repi(i,1,n-1){
			int u,v; si(u),si(v);
			add_edge(u,v),add_edge(v,u);
		}
		dfs(1,0);
		repi(i,1,n) son2[i]=son[i][0],siz2[i]=siz[i],pre2[i]=pre[i];
		dfs_2(1,0);
		printf("%lld\n",ans);
	}
	return 0;
}

2.P4582 求给定树有多少联通子树和这个树重心相同(若原树两个重心则联通子树也要两个中心)
看代码注释吧

int siz[MAX_N],w[MAX_N],mxsiz,totsiz;//???mxsiz?inf,totsiz???????
int k,rt1,rt2;
void get_barycenter(int u,int fa)//找原树重心
{
	siz[u]=1,w[u]=0;
	reps(u)if(v!=fa){
		get_barycenter(v,u),siz[u]+=siz[v];
		w[u]=max(w[u],siz[v]);
	}
	w[u]=max(w[u],totsiz-siz[u]);
	if(w[u]<mxsiz) mxsiz=w[u],k=1,rt1=u; //判断重心数目
	else if(w[u]==mxsiz) k=2,rt2=u;
}
int dp[MAX_N][MAX_N];//dp[i][j] 以i为根的子树一共带j个点的方案数
void dfs(int u,int fa)
{
	dp[u][0]=0,dp[u][1]=1,siz[u]=1;
	reps(u)if(v!=fa){
		dfs(v,u),siz[u]+=siz[v];
		repd(k,siz[u],1)repi(j,1,min(siz[v],k-1)) dp[u][k]=(dp[u][k]+dp[u][k-j]*dp[v][j]%mod)%mod;
	}//对dp数组的第二维要倒序更新,若正序会是先前的更新对之后的更新造成影响(选用了相同子树的节点)
}

int g[MAX_N][MAX_N];//g[i][j] 一共选i个节点 子树最大大小j的方案数
int solve_1()
{
	dfs(rt1,0);
	int ans=0,tot=0,mx=0;
	reps(rt1){
		tot+=siz[v],mx=max(mx,siz[v]);
		repd(j,tot,1)repd(u,min(j,siz[v]),1){//第二维也是倒序枚举
			if(j==u){ g[j][u]=(g[j][u]+dp[v][u])%mod; continue; }//特判j=u,因为g[j][0]=0
			for(int t=1;t<=mx&&t<=j;t++) g[j][max(t,u)]=(g[j][max(t,u)]+g[j-u][t]*dp[v][u]%mod)%mod;//枚举不同最大子树大小,注意判断,因为可能新加的u更大
		}
	}
	repi(j,1,tot)repi(t,1,mx)if(t*2<(j+1)) ans=(ans+g[j][t])%mod;//选j个节点 最大子树大小小于等于j/2的可行(重心定义)
	return ans+1;
}
int w2[MAX_N];//w2[i]选了i个节点的方案数
int solve_2()
{
	dfs(rt1,0);
	int ans=0,tot=0; w2[0]=1;
	reps(rt1)if(v!=rt2){
		tot+=siz[v];//w2最大只用维护到siz[rt2]-1,因为只用到这里
		repd(j,min(tot,siz[rt2]-1),1)repd(u,min(j,siz[v]),1) w2[j]=(w2[j]+w2[j-u]*dp[v][u]%mod)%mod;
	}
	//rt1为根的选了i个(不算rt1),rt2为根的选了i+1个(算上rt2)
	repd(i,siz[rt2]-1,0) ans=(ans+w2[i]*dp[rt2][i+1]%mod)%mod;
	return ans;
}
void init(int n)
{
	repi(i,0,n) head[i]=w2[i]=0;
	repi(i,0,n)repi(j,0,n) dp[i][j]=g[i][j]=0;
	tote=0;
}
int main()
{
	int T,cnt=1; si(T);
	while(T--)
	{
		int n; si(n); init(n);
		repi(i,1,n-1){
			int u,v; si(u),si(v);
			add_edge(u,v),add_edge(v,u);
		}
		mxsiz=inf,totsiz=n,get_barycenter(1,0);
		int ans=0;

		if(k==1) ans=solve_1();
		else ans=solve_2();
		printf("Case %d: %d\n",cnt++,ans%mod);
	}
	return 0;
}

你可能感兴趣的:(图论)