最长同值路径 图解题解
这道题到底在问什么
- 输入
- root=[5,4,5,1,1,5]
- 输出
- 2
- 输入
- root=[50,50,50,90,50,null,50](本帧这棵)
- 输出
- 4
最优解:为什么这么做
一句话答案:LeetCode 687 最长同值路径:后序 DFS 让每个孩子先交上向下的同值单臂,本节点用左臂加右臂刷答案、只把更长的单臂返给父亲,省掉每点重复下探,时间 O(n)。
同值路径数的是边,不是节点
给一棵二叉树 root,找一条路径,路径上每个节点的值都相等,返回它的边数,注意是边数不是节点数,5 个同值点连成一串算 4 条边。这条路径可以经过根,也可以整个躲在某棵子树里。题面 root=[5,4,5,1,1,5] 答案是 2,走的是右边 5-5-5 三个点两条边;root=[50,50,50,90,50,null,50] 答案是 4。
拿每个点当拐点向下探,深链会被反复数
路径能在任何一个节点拐弯,所以得让每个节点都当一次拐点:从它向左、向右各伸一条同值链,两条拼起来取最大。麻烦在于,量一个点向下的同值链要从它一路探到底;而它的孩子、孙子当拐点时,同一段链又被各自重探一遍。长链上的节点被翻来覆去数,最坏要 O(n²)。
让孩子先算完,把向下的单臂交上来
换个次序就能省掉重探。一个节点向下的最长同值链,只取决于它两个孩子向下的同值链,跟更上面的节点无关。于是改走后序 DFS:先递归两个孩子、拿到它们向下的链长,轮到当前节点时直接取用,不必重新下探。每个节点只在轮到自己时结算一次,一趟遍历搞定,时间落到 O(n)。
向下的单臂和跨过顶点的整条路,是两回事
递归函数返回一个数:从当前节点向下、值一路相同的最长链有多长,按边数计。规则是这样:左孩子存在且值和当前节点相等,左臂 l 就是左孩子返回值加一,否则记 0;右臂 r 同理看右孩子。
接着要分清两种量。过当前节点的同值路径允许从左臂拐到右臂,长度是 l+r,拿去刷新全局答案 ans;可返回给父亲的绝不能是 l+r——路径从父亲下来进了这个节点,只能继续朝一个孩子的方向延伸,在这里拐弯分叉就不成其为路径了。所以往上只交 max(l, r),两条臂里更长的那条。
拿 [5,4,5,1,1,5] 自底向上把每个节点的臂长结清
根是 5,左孩子 4 挂着两个叶子 1,右孩子 5 底下带一个叶子 5。后序先落到最底:两个叶子 1 向下无边,各返回 0;它们的父亲 4,左右孩子都是 1、和 4 不同值,两臂都归零,l+r=0,返回 0。
再看右边:叶子 5 返回 0;它父亲那个 5,左孩子 5 同值,左臂 l=0+1=1,没有右孩子 r=0,过它的路径 l+r=1,ans 刷成 1,上交 max(1, 0)=1。回到根 5:左孩子 4 不同值、左臂归零,右孩子 5 同值、右臂 r=1+1=2,过根的路径 l+r=0+2=2,ans 更新为 2,返回 max(0, 2)=2。跑完 ans=2,正是 5-5-5 那两条边。
空树、单点都是 0,最坑的是把边数成点数
复杂度上,每个节点后序访问、结算一次,做的是常数次比较和加法,时间 O(n);额外开销只有递归栈,深度就是树高 h——匀称时压到 O(log n),退化成一条链则到 O(n)。边界很干净:空树没有节点,答案 0;单个节点没有边,答案也是 0。
最常见的写反,是把返回值贪成 l+r 交给父亲——父亲据此会拼出一条分叉、实际走不通的路径,返回值必须收成 max(l, r)。另一处是把答案报成节点个数,可题目量的是边:5 个同值点之间只有 4 条边,数成 5 就整整多出一条。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记死这句:经过点取左臂加右臂刷答案,往上只返回更长的单臂。后面每帧都在套它。
- 4从根下探,叶子先结算先看清这棵树。根是 50,左右孩子也都是 50,只有左下角那个 90 跟大家不一样。后序 DFS 会一路下探到叶子,从最底下往上、一个个把节点结算掉。紫色是正在处理的节点,结算完会变绿。
- 5处理 idx 0下探到值为 50 的这个节点(紫色)。它还有孩子,得先把左右孩子递归算完,才能轮到它。
- 6处理 idx 1下探到值为 50 的这个节点(紫色)。它还有孩子,得先把左右孩子递归算完,才能轮到它。
- 7处理 idx 3下探到值为 90 的这个节点(紫色)。它没有孩子,是叶子,可以直接结算。
- 8idx 3 臂长 0叶子 90 向下一条边都没有,所以它给父亲的单臂长度是 0。它结算完毕,变绿。
- 9处理 idx 4下探到值为 50 的这个节点(紫色)。它没有孩子,是叶子,可以直接结算。
- 10idx 4 臂长 0叶子 50 向下一条边都没有,所以它给父亲的单臂长度是 0。它结算完毕,变绿。
- 11不同 → 左臂 0看左孩子,它的值是 90,跟当前的 50 不一样,这条边断掉,标红。所以左臂只能归零。
- 12同值 → 右臂 1再看右孩子,值同样是 50,同值,边接得上。右臂 = 右孩子返回的 0 加一条边 = 1。
- 13刷新答案 ans = 1把左臂 0 和右臂 1 拼起来,就是经过这个节点的最长同值路径,共 1 条边(紫路径)。它比之前的 ans 大,刷新答案为 1。
- 14往上只返回 max(0, 1) = 1结算完,它要回到父亲那一层。注意往上只能交一条臂,因为路径到了父亲不能在这里分叉,所以返回左右臂里更长的那条 = 1。本节点变绿,结束。
- 15处理 idx 2下探到值为 50 的这个节点(紫色)。它还有孩子,得先把左右孩子递归算完,才能轮到它。
- 16处理 idx 6下探到值为 50 的这个节点(紫色)。它没有孩子,是叶子,可以直接结算。
- 17idx 6 臂长 0叶子 50 向下一条边都没有,所以它给父亲的单臂长度是 0。它结算完毕,变绿。
- 18左臂 0当前节点没有左孩子,左边没有可延伸的臂,左臂记 0。
- 19同值 → 右臂 1再看右孩子,值同样是 50,同值,边接得上。右臂 = 右孩子返回的 0 加一条边 = 1。
- 20不超过 ans = 1把左臂 0 和右臂 1 拼起来,就是经过这个节点的最长同值路径,共 1 条边(紫路径)。它没超过已有的 ans = 1,答案保持不变。
- 21往上只返回 max(0, 1) = 1结算完,它要回到父亲那一层。注意往上只能交一条臂,因为路径到了父亲不能在这里分叉,所以返回左右臂里更长的那条 = 1。本节点变绿,结束。
- 22同值 → 左臂 2看左孩子,它的值也是 50,跟当前节点同值,这条边接得上。于是左臂 = 左孩子返回的 1 再加这一条边 = 2。左孩子标成路径色。
- 23同值 → 右臂 2再看右孩子,值同样是 50,同值,边接得上。右臂 = 右孩子返回的 1 加一条边 = 2。
- 24刷新答案 ans = 4把左臂 2 和右臂 2 拼起来,就是经过这个节点的最长同值路径,共 4 条边(紫路径)。它比之前的 ans 大,刷新答案为 4。
- 25往上只返回 max(2, 2) = 2结算完,它要回到父亲那一层。注意往上只能交一条臂,因为路径到了父亲不能在这里分叉,所以返回左右臂里更长的那条 = 2。本节点变绿,结束。
- 26答案 = 4 条边所有节点都结算完了。全局最长的「经过点」路径是紫色这条 50-50-50-50-50,从左下的 50 经过根再到右下的 50,正好 4 条边。那个 90 跟周围不同值,谁也接不上它,被排除在外。这就是答案。
⚠️ 容易写错的地方
✗ 错:返回时把左臂 + 右臂一起交给父亲
✓ 对:只能返回 max(左臂, 右臂)
路径到父亲不能在本节点分叉,交两条会让父亲算出不存在的路径
✗ 错:答案直接用返回值
✓ 对:答案要用「左臂 + 右臂」单独刷新
最长路径可能在本节点拐弯,跨左右,而返回值只是单边
✗ 错:数成了节点数
✓ 对:答案是边数 = 节点数减一
题目要的是路径长度(边数),5 个同值点是 4 条边
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def longestUnivaluePath(self, root: Optional[TreeNode]) -> int:
def dfs(root: Optional[TreeNode]) -> int:
if root is None:
return 0
l, r = dfs(root.left), dfs(root.right)
l = l + 1 if root.left and root.left.val == root.val else 0
r = r + 1 if root.right and root.right.val == root.val else 0
nonlocal ans
ans = max(ans, l + r)
return max(l, r)
ans = 0
dfs(root)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
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) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* 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 {
public:
int longestUnivaluePath(TreeNode* root) {
int ans = 0;
function<int(TreeNode*)> dfs = [&]( TreeNode* root ) -> int {
if (!root) {
return 0;
}
int l = dfs(root->left);
int r = dfs(root->right);
l = root->left && root->left->val == root->val ? l + 1 : 0;
r = root->right && root->right->val == root->val ? r + 1 : 0;
ans = max(ans, l + r);
return max(l, r);
};
dfs(root);
return ans;
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private int ans;
public int longestUnivaluePath(TreeNode root) {
ans = 0;
dfs(root);
return ans;
}
private int dfs(TreeNode root) {
if (root == null) {
return 0;
}
int l = dfs(root.left);
int r = dfs(root.right);
l = root.left != null && root.left.val == root.val ? l + 1 : 0;
r = root.right != null && root.right.val == root.val ? r + 1 : 0;
ans = Math.max(ans, l + r);
return Math.max(l, r);
}
}复杂度
时间
O(n)
每个节点只被后序访问并结算一次,常数次比较与加法
空间
O(h)
递归栈深 = 树高 h;最坏退化成链时 O(n),平衡时 O(log n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最长同值路径 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题和二叉树的直径 LeetCode 543 是不是一回事?+
骨架一模一样:都用后序 DFS,每个节点用左臂加右臂刷新全局答案、向上只返回更长的单臂。差别只在那条边算不算:543 求直径每条边都计入,687 只有孩子和父亲同值时这条边才算,不同值就把对应的臂清零。把 543 里无条件的加一换成同值才加一,就变成了 687。
为什么递归里不能直接返回左臂加右臂?+
返回值是留给父亲往上接的。路径从父亲走进当前节点后,只能顺着一个孩子的方向继续下去,不能在当前节点又拐向另一个孩子,否则它就分叉了、不再是一条路径。所以返回必须是单臂 max(l, r);而左臂加右臂那种在当前节点拐弯的路,到这个点已经是尽头,只能用来刷新全局答案、不能上交。
同值路径长度为什么按边数算,用节点数不行吗?+
题目定义的路径长度就是边数。5 个值相同的节点串起来,中间只有 4 条边,答案是 4 不是 5。代码里也自然对上:叶子向下没有边、返回 0;每往父亲接一层同值,臂长才加一,加的正是那一条边。若按节点数报,处处会多算一个,和题目对不上。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最长同值路径 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。