树的直径 两次dfs(起点到其余各点最大距离)、树形dp(各点到其子树叶子节点的最大距离)

定义

直径 : 在圆上两点(不相交)之间最远的距离就是我们通常所说的直径。
树的直径 : 树上最远的两个节点之间的距离就被称为树的直径,连接这两点的路径被称为树的最长链
(一开始,下意识地以为是,找到某个节点到它子树中叶子节点的最大距离,显然不是)

法一、两次dfs、bfs

思想

先从任意一点P出发,找离它最远的点Q,再从点Q出发,找离它最远的点W,W到Q的距离就是是的直径。

证明

①若P已经在直径上,根据树的直径的定义可知Q也在直径上且为直径的一个端点(从直径的一个端点Q出发,距离Q最远的点就是直径的另一个端点)
树上任意点能到的最远点,一定是树的直径的某个端点。

②若P不在直径上,我们用反证法,假设此时WQ不是直径,AB是直径

1、若AB与PQ有交点C,由于P到Q最远,那么PC+CQ>PC+CA,所以CQ>CA,易得CQ+CB>CA+CB,即CQ+CB>AB,与AB是直径矛盾,不成立,如下图(其中AB,PQ不一定是直线,画成直线是为了方便)
在这里插入图片描述
2、若AB与PQ没有交点,M为AB上任意一点,N为PQ上任意一点。首先还是NP+NQ>NQ+MN+MB,同时减掉NQ,得NP>MN+MB,易知NP+MN>MB,所以NP+MN+MA>MB+MA,即NP+MN+MA>AB,与AB是直径矛盾,所以这种情况也不成立,如下图:
在这里插入图片描述

优点

== 可以有效处理 负边权==

缺点

对于记录路径的信息效率较低

实现代码

#include<iostream>
#include <algorithm>
using namespace std;
const int N=4e4+10; 
struct edge{
	int to,w,nex;
}e[N<<1];
int head[N];
int cnt;
void add(int u,int v,int w){
	e[++cnt].to=v;
	e[cnt].w=w;
	e[cnt].nex=head[u];
	head[u]=cnt;
}
int Q;//通过节点1找到的直径的一个端点
int ans;//找节点1/Q到 其子树根节点的最大距离
int d[N];//1每个节点到其 
//这里不是 要求x到其子树根节点的最大距离(树形dp),
//而是距离1/Q距离最大的节点 
//要求距离x节点最远的节点,可以用d[y]记录它的所有能蔓延出去的节点
//到x的距离
//这样一来,距离最大的那个节点就是要找的节点 
//int vis[N];本来想优化dfs,找x的所有能到达的节点嘛
//但由于两节点之间只有一条路径(树),dfs下去不会重复,
//况且已经限制不能向上遍历了(向父亲) 
void dfs(int u,int fa){
		if(d[u]>ans){
			ans=d[u];//最大距离
			Q=u;//距离最初的u最远的节点 
		}
//反正可以通过d数组记录路径信息,可以dfs求出x所有能蔓延出去的节点
//到x的最远距离之后,d数组中最大元素对应的节点就是距离x最远的节点 
	for(int i=head[u];i;i=e[i].nex){
		int to=e[i].to;
		int w=e[i].w;
		if(to==fa)continue;//无向边,不要陷入循环了 
		d[to]=d[u]+w;//对于d数组,有点从上到下推的味道,so在dfs前 
		dfs(to,u);
	} 
} 
int main() {
	int n,m;
	cin>>n>>m;
//	道路不会交叉且每个农场间有且仅有一条路径
//	m只会是n-1 
	int u,v,w;
	char dir;
	for(int i=1;i<n;i++){
		cin>>u>>v>>w>>dir;
		add(u,v,w);
		add(v,u,w);
	} 
	dfs(1,0);//默认n个节点的编号分别为1~n,0当作根节点不存在的父节点
//	for(int i=1;i<=n;i++){
//		if(d[i]>ans){
//			ans=d[i];
//			Q=i;
//		}
//	} 
	fill(d+1,d+1+n,0);
	fill(vis+1,vis+1+n,0);
	ans=0;
	dfs(Q,0);
//	for(int i=1;i<=n;i++){
//		if(d[i]>ans){
//			ans=d[i];
//			Q=i;
//		}
//	} 
	cout<<ans; 
    return 0;
}

法二、树形dp

思想

1、先求出所有节点到其子树中叶子节点的最大距离d[x],
再对于每一个节点x,分别求出 d [ x ] + m a x ( d [ y i ] + e d g e [ y i ] ) d[x]+max(d[y_i]+edge[y_i])d[x]+max(d[yi]+edge[yi])
取每个节点得出结果的最大值

2、对于每一个节点x,求出x到其子树中叶子节点的最大距离f1[x],次长距离f2[x],
取每个节点得出f1[x]+f2[x]的最大值
对于每个节点我们要记录两个值:

f1 [ i ] 表示以 i 为根的子树中,i 到叶子结点距离的最大值

f2 [ i ] 表示以 i 为根的子树中,i 到叶子结点距离的次大值

对于一个节点,它到叶子结点距离的最大值和次大致所经过的路径肯定是不一样的
若j是i的儿子,那么(下面的 w [ i ][ j ] 表示 i 到 j 的路径长度):

若 f1 [ i ] < f1 [ j ] + w [ i ][ j ],f2 [ i ] = f1 [ i ],f1 [ i ] = f1 [ j ] + w [ i ][ j ];
否则,若 f2 [ i ] < f1 [ j ] + w [ i ][ j ],f2 [ i ] = f1 [ j ] + w [ i ][ j ];

理解:这样做就是,先看能否更新最大值,若能,它的次大值就是原先的最大值,再更新它的最大值;若不能,就看能不能更新次大值,若能,就更新,不能就不管它

这样的话,最后的答案 answer = max { f1 [ i ] + f2 [ i ] }

证明

我们不妨设1号点为根节点,那么这就可以看做一棵有根树。(设1号节点为根节点,那么一张N个点,N−1条边的无向图,我们可以认为它是一棵有根树)

1、可以求出从任意节点x出发,到以x为根的子树,能过到达最远节点(叶子节点)的距离
设D[x]表示从节点x出发,往以x为根的子树走,能够到达的最远距离。设x的子节点分别为y 1 , y 2 , y 3 , . . . , y t , e d g e ( x , y ) y1,y2,y3,...,yt,edge(x,y)y1,y2,y3,...,ytedge(x,y)表示从x到y的边权,则可以得到状态转移方程:D [ x ] = m a x ( D [ y i ] + e d g e ( x , y i ) ) D[x]=max(D[yi]+edge(x,yi))D[x]=max(D[yi]+edge(x,yi))

2、有了第一步求出的每个节点到自己子树中叶子节点的最大距离,我们考虑对于每个节点x求出经过x的最长链的长度F[x],整棵树的直径就是m a x F [ x ] ( 1 < = x < = n ) max{F[x]}(1<=x<=n)maxF[x](1<=x<=n)

现在我们考虑如何求F [ x ] 。 F[x]。F[x]
对于x节点的任意两个子节点y i 和 y j y_i和y_jyiyj,经过节点x的最长链的长度可以通过四个部分来构成

D [ y i ] D[y_i]D[yi]
D [ y j ] D[y_j]D[yj]
从 x 到 y i 的 距 离 从x到y_i的距离xyi
从 x 到 y j 的 距 离 从x到y_j的距离xyj
不 妨 设 j < i , 则 有 : 不妨设j<i,则有:j<i

F [ x ] = m a x ( D [ y i ] + D [ y j ] + e d g e ( x , y i ) + e d g e ( x , y j ) ) F[x]=max(D[y_i]+D[y_j]+edge(x,y_i)+edge(x,y_j))F[x]=max(D[yi]+D[yj]+edge(x,yi)+edge(x,yj))

非常需要理解的一点,不管是贪心思想还是数学证明,都需要直到,对于经过节点x的最长链的长度(直径是所有节点的最长链中的最大值),是

m a x ( x 经 过 子 节 点 y i 到 达 其 子 树 叶 子 节 点 的 最 大 值 + x 经 过 子 节 点 y j 到 达 其 子 树 叶 子 节 点 的 最 大 值 ) max(x经过子节点y_i到达其子树叶子节点的最大值+ x经过子节点y_j到达其子树叶子节点的最大值)max(xyi+xyj)(可表示为
x到其子树叶子节点的最大值+ m a x ( x 的 子 节 点 y i 到 其 子 树 叶 子 节 点 的 最 大 值 + e d g e ( x , y j ) ) max(x的子节点y_i到其子树叶子节点的最大值+edge(x,y_j))max(xyi+edge(x,yj))
显然,x到达其子树叶子节点的最长路径+次长路径
x到叶节点最长的两条路径
在这里插入图片描述

优点

可以通过一个新的数组记录路径信息(例如父节点与子节点之间的关系)

缺点

== 缺点 : 无法处理 负边权(遇到 负边权 凉凉)==

代码一 m a x ( 最 长 链 + 次 长 链 ) max(最长链+次长链)max+

#include<iostream>
#include <algorithm>
using namespace std;
const int N=4e4+10; 
struct edge{
	int to,w,nex;
}e[N<<1];
int head[N];
int cnt;
void add(int u,int v,int w){
	e[++cnt].to=v;
	e[cnt].w=w;
	e[cnt].nex=head[u];
	head[u]=cnt;
}
int f1[N];
int f2[N]; 
//f1,f2分别代表节点x到其子树节点的最大值和次大值
//两次dfs求直径中d数组是x节点能蔓延出去的所有节点到x的距离,应从上往下递推
//f1,f2显然应该从下往上递推 
int ans;//直径=max( d[u]
void dfs(int u,int fa){
	for(int i=head[u];i;i=e[i].nex){
		int to=e[i].to;
		int w=e[i].w;
		if(to==fa)continue;
		dfs(to,u);
		if(f1[u]<f1[to]+w){
			f2[u]=f1[u];//f2是次大值嘛,继承最大值上一次更新的值
			f1[u]=f1[to]+w; 
		} 
		else f2[u]=f1[to]+w;
		ans=max(ans,f1[u]+f2[u]);
	}
}
int main() {
	int n,m;
	cin>>n>>m;
//	道路不会交叉且每个农场间有且仅有一条路径
//	m只会是n-1 
	int u,v,w;
	char dir;
	for(int i=1;i<n;i++){
		cin>>u>>v>>w>>dir;
		add(u,v,w);
		add(v,u,w);
	} 
	dfs(1,0);
	cout<<ans; 

    return 0;
}

代码二max(x到子树叶子节点最长路径+x某子节点到子树叶子节点最长路径+x到该子节点距离)记得更新每个节点到子树叶子节点的最长距离

#include<iostream>
#include <algorithm>
using namespace std;
const int N=4e4+10; 
struct edge{
	int to,w,nex;
}e[N<<1];
int head[N];
int cnt;
void add(int u,int v,int w){
	e[++cnt].to=v;
	e[cnt].w=w;
	e[cnt].nex=head[u];
	head[u]=cnt;
}
int f1[N];
//int f2[N]; 
//f1,f2分别代表节点x到其子树节点的最大值和次大值
//两次dfs求直径中d数组是x节点能蔓延出去的所有节点到x的距离,应从上往下递推
//f1,f2显然应该从下往上递推 
int ans;//直径=max(f1[u]+f2[u])或max(f1[u]+f1[to]+w[u->to])
void dfs(int u,int fa){
	for(int i=head[u];i;i=e[i].nex){
		int to=e[i].to;
		int w=e[i].w;
		if(to==fa)continue;
		dfs(to,u);
//		if(f1[u]<f1[to]+w){
//			f2[u]=f1[u];//f2是次大值嘛,继承最大值上一次更新的值
//			f1[u]=f1[to]+w; 
//		} 
//		else f2[u]=f1[to]+w;
//		ans=max(ans,f1[u]+f2[u]);
		ans=max(ans,f1[u]+f1[to]+w);
		f1[u]=max(f1[u],f1[to]+w);
//		在逐一遍历x的各子节点过程中,随时可能发现一条更长的
//		x到其子树叶子节点的路,要更新f1[u]
//		因为 ans=max(ans,f1[u]+f1[to]+w)这个定义中
//		f1[x]就是x到其子树叶子节点的最长路 
	}
}
int main() {
	int n,m;
	cin>>n>>m;
//	道路不会交叉且每个农场间有且仅有一条路径
//	m只会是n-1 
	int u,v,w;
	char dir;
	for(int i=1;i<n;i++){
		cin>>u>>v>>w>>dir;
		add(u,v,w);
		add(v,u,w);
	} 
	dfs(1,0);
	cout<<ans; 

    return 0;
}

题目集

以上给出的代码对应的就是这道板子题
些许离谱,oj上的题面似乎不完整,连数据范围都没有
完整题面如下

【问题描述】
农夫约翰有N个农场,标号为1到N。M条不同的垂直或水平的道路连接着农场,道路的长度不超过1000。这些农场的分布就像下面的地图一样,图中农场用F1…F7表示:

每个农场最多能在东西南北四个方向连接4个不同的农场。此外,农场只处在道路的两端。道路不会交叉且每个农场间有且仅有一条路径。但是约翰丢了农场的地图,他只得从电脑的备份信息中修复了。每一条道路信息描述如下:

从农场23往南经距离10到达农场17
从农场1往东经距离7到达农场17
……

最近美国过度肥胖非常普遍,农夫约翰为了让他的奶牛多做运动,举办了奶牛马拉松。马拉松路线要尽量长,请帮助约翰寻找两个最远的农场间的距离。

【输入格式】
第1行两个整数N和M。
第2到第M+1行:每行包括4个分开的整数:F1,F2,L,D分别描述两个农场的编号,道路的长度,F1到F2的方向N,E,S,W。

【输出格式】
一个整数,表示最远两个农场的距离。

【输入样例】

7 6
1 6 13 E
6 3 9 E
3 5 6 S
4 1 3 N
2 4 20 W
4 7 2 S
1
2
3
4
5
6
7
【输出样例】

51
1
【样例解释】
最长的马拉松路线通过4,1,6,3到5;总长为:20+3+12+9+7=32。

【数据范围】
1<=N,M<=40000

环游世界

题意:
切断树的一条边,将树分为两棵,要让两棵树的最长路径(即直径)中较大的那个最小。

在这里插入图片描述

#include<iostream>
#include<vector>
#include <math.h>
using namespace std;
const int N=1e6+5;
struct edge{
	int to,nex,w;
}e[N<<1];
int head[N];
int cnt;
void add(int u,int v,int w){
	e[++cnt].to=v;
	e[cnt].w=w;
	e[cnt].nex=head[u];
	head[u]=cnt;
}
int Q,R;
int d[N];//每个节点到p的最远距离
int maxd;//存放最长的距离啦 
void dfs(int u,int fa){
	if(d[u]>maxd){
		maxd=d[u];
		Q=u;
	} 
	for(int i=head[u];i;i=e[i].nex){
		int to=e[i].to;
		int w=e[i].w;
		if(to==fa)continue;
		d[to]=d[u]+w;
		dfs(to,u);
	}
}
bool fg=false;
//dfsPath会深搜很多条路径,但只需要保存直径这条路径上的边
//怎么判断深搜的这条边是直径,
//1、这条路最后一个节点是R 2、这条路的长度为之前算出的maxd 
vector< pair<int,int> >v;
void dfsPath(int Q,int fa){
	if(Q==R){
		fg=true; 
		return;
	}
	for(int i=head[Q];i;i=e[i].nex){
		int to=e[i].to;
		int w=e[i].w;
		if(to==fa)continue;
		v.push_back(make_pair(Q,to));//顺序有讲究,断开这条边,pair左边的节点是在Q所属的那颗子树里 
		dfsPath(to,Q);
		if(fg)return;
		v.pop_back();
		//不这条路是直径啦 ,这一路走来所有保存的边都要抛除去 
	}
} 
//树形dp求每一棵子树的直径
//通过每个节点到其子树叶子节点的最大距离(最长链而不是最长路径,直径) 
int f[2][N];//记录每个节点到其子树叶子节点的最大距离,最长链 
//int mx;求树的直径板子题里,就是求给定的一棵树的直径,
//mx相当于mx[root],以root为根的最长路径,直径,用一个遍历mx足矣
//但这里要求,求以任意节点为根的子树的直径(以删除的边的两节点为根,删除的边是任意的)
int mx[2][N];//回顾一下mx的求法 
//1、mx=max(mx,f[u]+f[v]+w),f[u]=max(f[u],f[v]+w)
//2、f1[u]或f2[u]=f[v]+w,mx=max(mx,f1[u]+f2[u]); 
//注意啦,f[i]是i到其子树叶子的最长路径
//mx[i]是以i为根的树的最长链即直径(是i到叶子的最长路径+次长路径) 
void dfs2(int u,int fa,int flag){//Q所在的半颗子树的众多子树的最长链 
//	mx[flag][u] = f[flag][u] = 0;
	for(int i=head[u];i;i=e[i].nex){
		int to=e[i].to;
		int w=e[i].w;
		if(to==fa)continue;
		dfs2(to,u,flag);
		mx[flag][u]=max(mx[flag][u],f[flag][u]+f[flag][to]+w);
		f[flag][u]=max(f[flag][u],f[flag][to]+w);
		//随着遍历u的各孩子,要更新u到叶子的最长链 
	}
//要与u所有的子树中最大直径比较,原来只用一个全局变量ans,即mx[root]
//ans在每一层递归中都会在与求出来的最长路径比较
//而今用mx数组代替ans,一来可以表示以每个节点为根的子树直径,但
//mx[u]已经不能再全局比较
//so递归的过程中u必须与它的孩子的最长路径比较,因为它的孩子
//已经与孩子的孩子进行了比较,握住了自他以下的最长路径 
	for(int i=head[u];i;i=e[i].nex){
		int to=e[i].to;
		mx[flag][u]=max(mx[flag][u],mx[flag][to]);
	}
}
//void dfs2(int u,int fa,int flag)
//{
//    int mx1=0,mx2=0;
//    for(int i=head[u];i;i=e[i].nex){
//        int v=e[i].to;
//        int w=e[i].w;
//        if(v==fa)continue;
//        dfs2(v,u,flag);
//        int t=f[v][flag]+w;
// 
//        if(t>=mx1) mx2=mx1,mx1=t;
//        else if(t>mx2) mx2=t;
// 
//        f[u][flag]=max(f[u][flag],f[v][flag]+w);
//        mx[u][flag]=max(mx[u][flag],mx[v][flag]);
//    }
//    //if(!flag) printf("u:%d mx1:%d mx2:%d\n",u,mx1,mx2);
//    mx[u][flag]=max(mx[u][flag],mx1+mx2);
//}

int main() {
//最大距离的最小值,一定要切断直径,即删除的边在直径上(要保存直径这条路径)
//切断一条边后,变成两颗树,要求这两棵树中所有的子树的最长直径
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin>>n;
int x,y,w;
for(int i=0;i<n-1;i++){
	cin>>x>>y>>w;
	add(x,y,w);
	add(y,x,w); 
} 
//1、欲遍历直径上的所有边,先找到直径的两端点
//两次dfs求直径(距离p最远的点Q是直径一个端点,距离Q最远的点是另一个端点R)
	dfs(1,0);
	fill(d+1,d+1+n,0);
	maxd=0;
	R=Q;//先保存一个端点 
	dfs(Q,0); 
	dfsPath(Q,0);
	
	int len=v.size();
	dfs2(Q,0,0);//dfs2用于求以每个节点为根节点的子树的最长路径
//	但随着割断的边不同,每个节点指不定属于Q,R哪一边
//	属于两边的情况都要求,第三个参数用于标记该节点属于哪一边时的直径 
	dfs2(R,0,1);
	int res=0x3f3f3f3f;//要搞无穷大就要写齐4个3f啦,不然直接1e9 
	for(int i=0;i<len;i++){
		int temp=max(mx[1][v[i].first],mx[0][v[i].second]);
		res=min(res,temp);
	}
	cout<<res;
    return 0;
}

参考思路
参考代码

参考

树的直径
树形dp的第二种代码的解释
树的直径方法总结
再看看别的大佬的讲解
树的直径讲义


版权声明:本文为qq_51070956原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。