题目描述
思路解析动画文字版
记住这条主线:DFS 返回的是「子树和」,坡度是顺路在每个节点结算时累加的副产品。
准备 · 从根开始后序遍历:开局累计坡度是 0。后序 DFS 从根 50 出发,但根的坡度现在算不了,得先知道左右子树各自的和。所以先一路往左下钻,钻到最左下的叶子,再自底向上一个个结算。盯住右边的坡度结算面板,每结算一个内部节点,累计坡度就涨一截。
下钻 · 进入 50:递归进入节点 50(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
下钻 · 进入 20:递归进入节点 20(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
下钻 · 进入 30:递归进入节点 30(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
下钻 · 到叶子 80:递归走到节点 80(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
结算叶子 80 · 坡度 0:结算叶子 80:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 80。节点变绿表示处理完。
下钻 · 到叶子 10:递归走到节点 10(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
结算叶子 10 · 坡度 0:结算叶子 10:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 10。节点变绿表示处理完。
结算 30 · 坡度 70:节点 30 的左右子树和都齐了:左边 80,右边 10。本节点坡度 = |80-10| = 70,累加进总答案,累计坡度涨到 70。它往上交的子树和 = 30+80+10 = 120。节点变绿。
下钻 · 到叶子 90:递归走到节点 90(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
结算叶子 90 · 坡度 0:结算叶子 90:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 90。节点变绿表示处理完。
结算 20 · 坡度 30:节点 20 的左右子树和都齐了:左边 120,右边 90。本节点坡度 = |120-90| = 30,累加进总答案,累计坡度涨到 100。它往上交的子树和 = 20+120+90 = 230。节点变绿。
下钻 · 进入 70:递归进入节点 70(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
下钻 · 到叶子 60:递归走到节点 60(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
结算叶子 60 · 坡度 0:结算叶子 60:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 60。节点变绿表示处理完。
下钻 · 到叶子 40:递归走到节点 40(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
结算叶子 40 · 坡度 0:结算叶子 40:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 40。节点变绿表示处理完。
结算 70 · 坡度 20:节点 70 的左右子树和都齐了:左边 60,右边 40。本节点坡度 = |60-40| = 20,累加进总答案,累计坡度涨到 120。它往上交的子树和 = 70+60+40 = 170。节点变绿。
结算 50 · 坡度 60:节点 50 的左右子树和都齐了:左边 230,右边 170。本节点坡度 = |230-170| = 60,累加进总答案,累计坡度涨到 180。它往上交的子树和 = 50+230+170 = 450。节点变绿。
汇总 · 把每个节点坡度相加:九个节点全部结算完。看右边的坡度面板:五个叶子坡度都是 0,对总和没贡献;真正出力的是四个内部节点,坡度分别是节点 30 的 70、节点 20 的 30、节点 70 的 20、根 50 的 60。把它们相加就是最终答案。
完成 · 累计坡度就是答案:70 加 30 加 20 加 60,正好等于 180,这就是整棵树的坡度。回头看,整个过程只走了一遍后序遍历,子树和一路往上递,坡度顺路累加,一次搞定,时间是 O(n)。
边界先想清:空树和单节点都是 0;坡度只在有孩子的节点上才可能非 0。
面试常追问后序的必然性和爆栈应对,核心都绕着「先子后父」。
参考代码
from __future__ import annotationsfrom 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 = rightclass 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 = rightclass Solution: def findTilt(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) nonlocal ans ans += abs(l - r) return l + r + root.val ans = 0 dfs(root) return ans复杂度
- 时间:O(n),后序遍历每个节点只被访问、结算一次,算和与算坡度都是常数操作
- 空间:O(h),只用递归栈,深度等于树高 h;最坏(链状树)退化到 O(n),平衡树则是 O(log n)
易错点
面试追问把动画讲成自己的话
追问为什么不能一遍前序就把坡度都算出来?
追问如果树非常深,递归会爆栈吗?怎么改?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
另一棵树的子树
LeetCode 572 · 简单 · 沿着 树套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题