所有文字著作权归本人所有,禁止转载抄袭!如有错误,欢迎指正!
很久之前看过求树的直径这个问题,但是那时还看不太懂实现方法。后来学会了树形dp,偶然间又在leetcode上刷了两个有关二叉树的题,发现和求树的直径这个问题很相似,只是把树特殊化成了二叉树,然后加了一些变化。原本写这篇总结时想先引入那两个简单题,再过渡到树的直径这个问题。但是想想那两个题并非是“严格的”求解树的直径的问题。这样做对于讲树的直径来说就本末倒置了。故本篇总结先讲解树的直径问题,最后附上两个简单题有助于对比理解。
题目描述:
题目链接 洛谷U81904 树的直径
给定一棵树,树中每条边都有一个权值,
树中两点之间的距离定义为连接两点的路径边权之和。
树中最远的两个节点之间的距离被称为树的直径,连接这两点的路径被称为树的最长链。
现在让你求出树的最长链的距离
题意总结:求树上距离最远的两个节点间的距离
输入格式
给定一棵无根树
第一行为一个正整数n,表示这颗树有n个节点
接下来的n−1行,每行三个正整数u,v,表示u,v(u,v<=n)有一条权值为w的边相连
数据保证没有重边或自环
输出格式
输入仅一行,表示树的最长链的距离
输入输出样例
输入
6
1 2 1
1 3 2
2 4 3
4 5 1
3 6 2
输出
9
说明/提示
对于100%的数据 n<=500000 边权可能为负
一.什么是树形dp?
树形dp就是在树结构上所做的动态规划,通常树形dp是基于dfs来实现的。在用dfs遍历节点并维护信息的时候采用“后序”的思想:对于每个节点,都要用其所有儿子节点的信息来更新它自身的信息,只有当所有儿子节点的信息都得到确认之后,父节点的信息才能得到确认。基于这种“后序”的思想,树形dp通常具有如下结构:
/ /树形dp结构伪代码描述
void dfs(节点u){
for(){ / /循环访问所有u的子节点
dfs(u的子节点);
用u的子节点信息更新节点u的信息;
}
}
二.用树形dp求树的直径:
既然是树形动态规划,我们就尝试用上面树形dp的框架来解决问题。
1.首先,要确定维护的信息是什么?
假设当前父节点是 u u u, u u u的所有儿子节点为 v 1 , . . . , v n v_{1},...,v_{n} v1,...,vn,那么这个信息必然要满足“只要知道了儿子节点 v 1 , . . . , v n v_{1},...,v_{n} v1,...,vn的该信息,就能确定u的该信息”。由于最终要求树上最远两个节点的距离,不妨做这样的定义:设d[x]为节点x到其子孙节点的最大距离、设f[x]为以x为根结点的一条最长路径的距离。即要维护的信息就是d[],f[]。
2.如何维护上述信息?
(1)假设当前遍历到的节点是u,u的子节点是 v 1 , . . . , v n v_{1},...,v_{n} v1,...,vn,对应的边权是 w 1 , . . . , w n w_1,...,w_n w1,...,wn.依据树形dp的“后序”思想,继续假设已经求得了u的所有子节点 v 1 , . . . , v n v_{1},...,v_{n} v1,...,vn到其子孙节点的最大距离d[ v 1 v_{1} v1],…,d[ v n v_n vn]。已知信息画成下图,其中红色箭头所示的边为虚拟的边,也可看成是一条路径。
(2)根据已知信息求d[u]:若边权都是正值,则d[u]=max(d[ v 1 v_1 v1]+ w 1 w_{1} w1,d[ v 2 v_2 v2]+ w 2 w_{2} w2,…,d[ w n w_n wn+ w n w_n wn]),若存在负的权值,则d[u]=max(0, w 1 w_1 w1,d[ v 1 v_1 v1]+ w 1 w_{1} w1, w 2 w_2 w2,d[ v 2 v_2 v2]+ w 2 w_{2} w2,…, w n w_n wn,d[ v n v_n vn]+ w n w_n wn),可见d[u]>=0。
(3)确定f[u]的值:若边权都是正值,则f[u]=(d[ v x v_x vx]+ w x w_x wx)+(d[ v y v_y vy]+ w y w_y wy),其中(d[ v x v_x vx]+ w x w_x wx)和(d[ v y v_y vy]+ w y w_y wy)分别是u能到达子孙节点的最远距离和次远距离。即f[u]=d[u]+(d[ v y v_y vy]+ w y w_y wy)。若存在负的权值,则f[u]=max(0,d[u]+d[ v y v_y vy]+ w y w_y wy,d[u]+max( w 1 w_1 w1,…, w x − 1 w_{x-1} wx−1, w x + 1 w_{x+1} wx+1, w n w_n wn)),其中d[u]=d[ v x v_x vx]+ w x w_x wx,可见f[u]>=0。 说句人话就是:当权值为正值时,选择以u为根节点的两条最长路径和次长路径相加,得到以u为根结点的最长路径值。当权值存在负值时,就在下面两种组合中选择一个最大值:<1>最长路径+次长路径 <2>最长路径+某一条边
3.确定整棵树的最大距离res:res=max(f[1],…,f[n])。
4.正确性证明:
可以想象,最长的路径构成的子树必然有且仅有一个根,即最靠近整棵树的根的那个节点。上述算法遍历了整棵树,求得了以所有节点为根结点的最长路径,而整棵树的最长路径有且仅有一个根,必然包含在“以所有节点为根结点的最长路径”中。 换句话说,假设我们在树上标出这条最短路,这条最短路必然有且仅有一个根节点。而遍历树的时候也必然会遍历到这个根节点并求出这个节点的“以该节点为根的最长路径”并放到f[]数组中,在f[]中找个最大值不就找到了。
5.总结:整个算法的思想就是假设当前节点就是最长路径的根,并求得以当前节点x为根的最长路径f[x]。最后在分别以节点1,…,n为根节点的最长路径 f[1],…,f[n]中选择一条最长的即可。
最后两条描述可以结合下面的图来理解,假设图中连接a和b的路径是树的最长链,这条路径有且仅有一个根节点u(u是这条路径上离root最近的节点)。其它每个节点充当根结点时都对应一个“局部的最长路”,比如图中连接z和y的路径是以x为根节点的局部最长路,只是没有a->b长而已。
6.核心代码:分两种写法,第一种严格按上面的描述定义变量。第二种写法只是第一种的改版——让dfs返回变量d,并在在遍历时更新res。附上第二种写法的目的在于对比后面两个leetcode题的解法。
/ /树形dp解树的直径的两种写法:
/ /第一种写法
int vis[N];
int res=INT_MIN; / /初始为一个很小的值
int d[N],f[N];
void dfs(int u){
vis[u]=1;
for(int i=head[u];i>=0;i=e[i].next){ //遍历u的出边
int v=e[i].v;
int w=e[i].w;
if(!vis[v]){ //如果时u的子节点
dfs(v);
f[u]=max(max(f[u],d[u]),max(d[v]+w,d[u]+d[v]+w));
d[u]=max(max(d[u],w),d[v]+w);
}
}
}
dfs(1);
for(int i=1;i<=n;i++) res=max(res,f[i]);
printf("%d\n",res);
/ /第二种写法:
int vis[N];
int res=INT_MIN;
int dfs(int u){
vis[u]=1;
int ud=0;
for(int i=head[u];i>=0;i=e[i].next){
int v=e[i].v;
int w=e[i].w;
if(!vis[v]){
int vd=dfs(v);
res=max(max(res,ud),max(vd+w,ud+vd+w));
ud=max(max(ud,w),vd+w);
}
}
return ud;
}
//C++代码
//采用链式前向星存图
#include
#include
#include
#include
using namespace std;
const int N=5e5+100;
struct edge{
int u;
int v;
int w;
int next;
}e[2*N];
int head[N],cnt=0;
void Insert(int u,int v,int w){
cnt++;
e[cnt].u=u;
e[cnt].v=v;
e[cnt].w=w;
e[cnt].next=head[u];
head[u]=cnt;
}
int vis[N];
int res=INT_MIN;
int d[N],f[N];
void dfs(int u){
vis[u]=1;
for(int i=head[u];i>=0;i=e[i].next){
int v=e[i].v;
int w=e[i].w;
if(!vis[v]){
dfs(v);
f[u]=max(max(f[u],d[u]),max(d[v]+w,d[u]+d[v]+w));
d[u]=max(max(d[u],w),d[v]+w);
}
}
}
int main(){
memset(head,-1,sizeof(head));
int n;
scanf("%d",&n);
int u,v,w;
for(int i=1;i<=n-1;i++){
scanf("%d%d%d",&u,&v,&w);
Insert(u,v,w);
Insert(v,u,w);
}
dfs(1);
for(int i=1;i<=n;i++) res=max(res,f[i]);
printf("%d\n",res);
return 0;
}
题目链接:124. 二叉树中的最大路径和
路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径至少包含一个 节点,且不一定经过根节点。
路径和是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
提示:
树中节点数目范围是 [1, 3 * 104], -1000 <= Node.val <= 1000
模板题中的路径和是路径上边的权值,而此处是路径上点的权值。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
int res=INT_MIN;
public:
int dfs(TreeNode *root){
if(root==nullptr) return 0;
int d=root->val;
int l=dfs(root->left);
int r=dfs(root->right);
res=max(max(res,d+l+r),max(d,max(d+r,d+l)));
d=max(d,max(d+l,d+r));
return d; //返回 以当前节点为根的到达子孙节点的最长路径和
}
int maxPathSum(TreeNode* root) {
dfs(root);
return res;
}
};
题目链接:leetcode 543. 二叉树的直径
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
注意:两结点之间的路径长度是以它们之间边的数目表示。
路径长度改成路径上边的数目。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
int res=INT_MIN;
public:
int dfs(TreeNode* root){
if(root==nullptr) return 0;
int d=0;
int l=dfs(root->left);
int r=dfs(root->right);
d=max(l+1,r+1);
res=max(res,l+r);
return d;
}
int diameterOfBinaryTree(TreeNode* root) {
dfs(root);
return res;
}
};