【树】【动态规划】关于树上的一些问题

树的直径

树的直径,又称树的最长链,定义为一棵树上最远的两个节点的路径,即树上一条不重复经过某一条边的最长的路径。树的直径也可以代指这条路径的长度。

  • 树上任意点能到的最远点,一定是树的直径的某个端点。

求解树的直径有两种方法 时间复杂度都为 O ( n ) O(n) O(n)

  • 方法一 两遍搜索:从树上任意点u开始DFS(BFS)遍历图,得到距离u最远的结点v,然后从v点开始DFS遍历图,得到距离v最远的结点w, 则v、w之间的距离就是树的直径。
  • 方法二 树形dp:DP:显然最长路的两个端点必然是叶子或者根节点。设f(i)表示到i最远的叶子,g(i)表示到i次远的叶子,则有
    f ( i ) = m a x   f ( j ) + 1 f(i)=max{\ f(j)}+1 f(i)=max f(j)+1
    g ( i ) = s e c o n d m a x   f ( j ) + 1 g(i)=second_{max}{\ f(j)}+1 g(i)=secondmax f(j)+1
    其中j必须是i的儿子,计算顺序是自底向上。最终答案为
    m a x   f ( i ) + g ( i )   + 1 max{\ f(i)+g(i)\ }+1 max f(i)+g(i) +1
  • dp方法的优化:可以直接用一个 p r e pre pre 记录当前节点u 经过遍历到v之前的子节点 到叶子结点的最大路径,且 当前节点u到当前子节点v 到叶子节点的路径长 d p [ v ] + e [ i ] . w dp[v]+e[i].w dp[v]+e[i].w ,因此可得一条树的直径为 p r e + d p [ v ] + e [ i ] . w pre+dp[v]+e[i].w pre+dp[v]+e[i].w ,与ans取max,再更新pre使其与 d p [ v ] + e [ i ] . w dp[v]+e[i].w dp[v]+e[i].w 取max。注意两次更新的顺序。
#include<iostream>
#include<cstdio>
using namespace std;
int n,cnt,ans;
const int N=1e4+5;
const int M=2*N;
int fa[N],dp[N],head[N];
struct node{
    int to,next,w ;
}e[M];

void add(int u,int v,int w){
    e[++cnt].to =v; e[cnt].w =w; e[cnt].next=head[u]; head[u]=cnt;
}

void dfs(int u){
    int pre=0; //表示以当前节点u向下走的最大路径 即dp[u]
    for(int i=head[u];i;i=e[i].next ){
        int v=e[i].to ;
        if(v==fa[u]) continue; 
        fa[v]=u;
        dfs(v);
        ans=max(ans,pre+dp[v]+e[i].w); //一条经过u和v的树的直径
        pre=max(pre,dp[v]+e[i].w); //u经过v及其之前子节点中 到叶子结点的最大路径
    }
    dp[u]=pre;
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w); add(v,u,w);
    }
    dfs(1);
    printf("%d",ans);
}

树的中心

定义:树上任意一条直径所在的链的中点,当直径为偶数的时候,中心由一个点 u u u 构成。当直径为奇数的时候,中心由两个点构成 ( u , v ) ( u , v ) (u,v)

性质

  • 树的中心是唯一的,有一个且仅有一个中心
  • 树上任意一个节点 u u u到另外一个任意节点 v v v的的距离是最大值,那么 v v v为树上直径两个端点的其中一个。

带边权树的中心

给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。
请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。

树的重心

定义:树上的每一个节点都有一个平衡值,该平衡值的定义为,以该节点为根节点,所有子树中节点数量最大的那个子树中节点数量。树的重心是树上的一个节点,其平衡值是树中节点中最小的那个。

性质

  • 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。

  • 树上所有的点到树的重心的距离之和是最短的,如果有多个重心,那么总距离相等。

  • 插入或删除一个点,树的重心的位置最多移动一个单位。

  • 若添加一条边连接2棵树,那么新树的重心一定在原来两棵树的重心的路径上。

求解树的重心,核心框架是dfs

#include<iostream>
#include<cstdio>
using namespace std;
int n,cnt,ans=1e9;
const int M=11000,N=2100;
int st[N],head[N];
struct node {
	int to,next;
} e[M];
void add(int u,int v) {
	e[++cnt].to=v; e[cnt].next=head[u]; head[u]=cnt;
}

int dfs(int u) {
	int size=0; //连通块中点的最大值 即当前根的子树大小与另一连通块大小中 较大的一个 
	int sum=1;//以u为根的子树大小 
	st[u]=1;
	for(int i=head[u]; i; i=e[i].next) {
		int v=e[i].to ;
		if(!st[v]){
			int f=dfs(v); //以v为根的子树大小 
			size=max(size,f);
			sum+=f; 
		} 
	}
	size=max(size,n-sum);//取以u为根的子树的最大值 
	ans=min(ans,size); //取最大子树的最小值 
	return sum;
}

int main() {
	scanf("%d",&n);
	for(int i=1; i<n; i++) {
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v);
	}
	printf("%d",dfs(1));
}

例题:P1395 会议
给定一棵无根树,求多个重心中编号最小的一个,并求出各个点到该重心的距离之和。
在模改的过程中注意各个变量的意义。

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
int n,cnt,ans=1e9,pos,res;
const int M=101000,N=51000;
int st[N],head[N],dep[N];
struct node {
	int to,next;
} e[M];
void add(int u,int v) {
	e[++cnt].to =v;e[cnt].next =head[u];
	head[u]=cnt;
}

int dfs(int u,int fa) {
	int size=0; //当前根的子树大小与另一连通块大小中 较大的一个 
	int sum=1;//以u为根的子树大小 
	st[u]=1;
	for(int i=head[u]; i; i=e[i].next) {
		int v=e[i].to ;
		if(!st[v]&&fa!=v){
			int f=dfs(v,u); //以v为根的子树大小 
			size=max(size,f);
			sum+=f; 
		} 
	}
	size=max(size,n-sum);
	if(ans>size){
		ans=size, pos=u; //更新重心 
	} 
	else if(ans==size) //多个重心选取编号较小的一个 
		pos=min(u,pos);  
	return sum;
}
void bfs(int x){
	queue<int>q;
	q.push(x);
	res=0;
	while(!q.empty()){
		int u=q.front(); q.pop();
		for(int i=head[u];i;i=e[i].next ){
			int v=e[i].to;
			if(!dep[v]&&v!=x){
				dep[v]=dep[u]+1;
				res+=dep[v];
				q.push(v);
			}
		}
	}
}
int main() {
	scanf("%d",&n);
	for(int i=1; i<n; i++) {
		int u,v;
		scanf("%d%d",&u,&v);
		add(u,v); add(v,u);
	}
	dfs(1,0);
	bfs(pos);
	printf("%d %d",pos,res);
	
}

带点权树的重心

即树中每个节点带点权,求树的重心

状态表示 f [ u ] f[u] f[u]表示以u为根的总距离, s i z e [ u ] size[u] size[u]表示以u为根的子树的大小(结点数乘以权值)。
状态计算:对于每个u能达到的点v,有:
f [ v ] = f [ u ] + s i z e [ 1 ] − s i z e [ v ] − s i z e [ v ] f[v]=f[u]+size[1]-size[v]-size[v] f[v]=f[u]+size[1]size[v]size[v]
画棵树模拟一下就明白了
显然, a n s = m i n ( f [ i ] , 1 < = i < = n ) ans=min(f[i],1<=i<=n) ans=min(f[i],1<=i<=n)
任意以一个点为根dfs一遍,求出以该点为根的总距离。方便起见,我们就以1为根。

例题:P1364 医院设置

注意预处理和dp过程两个递归方式的不同,有助于更好的理解递归

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1100; 
int n,ans=1e9,cnt;
int f[N],w[N],size[N],head[N];
struct node{
	int to,next;
}e[N<<2];

void add(int u,int v){
	e[++cnt].to=v; e[cnt].next =head[u]; head[u]=cnt;
}

void dfs(int u,int fa,int dep){
	size[u]=w[u]; //初始化带点权 
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to ;
		if(v!=fa){
			dfs(v,u,dep+1); //自底向上 先递归到最底层 再累加计算 
			size[u]+=size[v]; //预处理子树大小 带点权 
		}
	}
	f[1]+=w[u]*dep;//预处理 f[1] 
}
void dp(int u,int fa){
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].to;
		if(v!=fa){
			f[v]=f[u]+size[1]-size[v]-size[v]; //状态转移 
			dp(v,u); //由于是u->v 正推 所以先转移状态 再递推 
		}
	}
	ans=min(ans,f[u]); //ans 
	return;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		int u,v;
		scanf("%d%d%d",&w[i],&u,&v);
		if(u!=0) add(u,i),add(i,u);
		if(v!=0) add(v,i),add(i,v);
	}
	dfs(1,0,0);
	dp(1,0);
	printf("%d",ans);
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值