树形dp求树的直径

所有文字著作权归本人所有,禁止转载抄袭!如有错误,欢迎指正!

引言

很久之前看过求树的直径这个问题,但是那时还看不太懂实现方法。后来学会了树形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就是在树结构上所做的动态规划,通常树形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]。已知信息画成下图,其中红色箭头所示的边为虚拟的边,也可看成是一条路径。
图1(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} wx1, 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++代码

//C++代码
//采用链式前向星存图
#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>
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;
}


leetcode变形题一

题目链接: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

分析

模板题中的路径和是路径上边的权值,而此处是路径上点的权值。

C++代码

/**
 * 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变形题二(更简单)

题目链接:leetcode 543. 二叉树的直径

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
示例 :
给定二叉树

      1
     / \
    2   3
   / \     
  4   5    

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

注意:两结点之间的路径长度是以它们之间边的数目表示。

分析

路径长度改成路径上边的数目。

C++代码


/**
 * 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;
    }
};






  • 15
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
树形动态规划(Tree DP)是一种常用的动态规划算法,用于解决结构相关的问题。在Python中,可以使用递归或者迭代的方式实现树形DP树形DP的基本思想是,从的叶子节点开始,逐层向上计算每个节点的状态,并利用已经计算过的节点状态来更新当前节点的状态。这样可以通过自底向上的方式,逐步计算出整个的最优解。 下面是一个简单的示例,演示如何使用树形DP解决一个二叉中节点权值之和的最大值问题: ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_sum(root): if root is None: return 0 # 递归计算左右子的最大权值和 left_sum = max_sum(root.left) right_sum = max_sum(root.right) # 当前节点的最大权值和为当前节点值加上左右子中较大的权值和 return root.val + max(left_sum, right_sum) # 构建一个二叉 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.left.left = TreeNode(4) root.left.right = TreeNode(5) # 计算二叉中节点权值之和的最大值 result = max_sum(root) print(result) ``` 这段代码中,我们定义了一个`TreeNode`类来表示二叉的节点,其中`val`表示节点的权值,`left`和`right`分别表示左子节点和右子节点。`max_sum`函数使用递归的方式计算二叉中节点权值之和的最大值,通过比较左右子的最大权值和来确定当前节点的最大权值和。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值