统计最高分的节点数目 图解题解
这道题到底在问什么
- 输入
- parents=[-1,2,0,2,0]
- 输出
- 3
- 输入
- parents=[-1,2,0]
- 输出
- 2
先想最直接的笨办法
五个节点的子树大小都到手了:节点 0 是 5,节点 2 是 3,节点 1、节点 3、节点 4 各是 1。有了这张表,接下来就能一个一个删节点、算分数。约定好评分口径:删掉节点后剩下的每一块大小相乘。下面按节点编号 0 到 4 的顺序逐个算。(动画第 10 步)
最优解:一步一步想明白
- 3记牢这一句:分数等于删掉该节点后各块大小的乘积。块分两种,一种是它的每棵子树,大小直接是子树 size;另一种是上方剩下的那块,大小是 n 减去它的子树大小。下面先把五个节点的子树大小求出来。
- 4n = 5 · 目标:每个节点的分数这就是要处理的树。节点 0 是根,它有两个孩子节点 2 和节点 4;节点 2 底下又挂着节点 1 和节点 3;节点 4、节点 1、节点 3 都是叶子。一共 5 个节点。第一步不急着算分,先自底向上把每个节点的子树大小求出来,后面算分全靠它。
- 5节点 1 子树大小 = 1先看最底下的叶子节点 1。它没有孩子,子树里就它自己一个,子树大小是 1。叶子的子树大小永远是 1,这是递归的底。
- 6节点 3 子树大小 = 1同样,叶子节点 3 也没有孩子,子树大小是 1。它和节点 1 是节点 2 的两个孩子。
- 7节点 2 子树大小 = 3轮到节点 2。它的子树包含它自己,加上左孩子节点 1 的子树 1 个、右孩子节点 3 的子树 1 个,所以子树大小是 1 加 1 加 1,等于 3。这三个绿色节点就是节点 2 那一整棵子树。
- 8节点 4 子树大小 = 1再看叶子节点 4,它挂在根节点 0 下面,没有孩子,子树大小是 1。
- 9节点 0 子树大小 = 5最后是根节点 0。它的子树就是整棵树,自己 1 个,加上节点 2 那棵 3 个、节点 4 那棵 1 个,子树大小是 1 加 3 加 1,等于 5,正好是全部节点数。
- 10五个 size 已求出五个节点的子树大小都到手了:节点 0 是 5,节点 2 是 3,节点 1、节点 3、节点 4 各是 1。有了这张表,接下来就能一个一个删节点、算分数。约定好评分口径:删掉节点后剩下的每一块大小相乘。下面按节点编号 0 到 4 的顺序逐个算。
- 11正在删:节点 0第一个算根节点 0。把节点 0 和它的两条边删掉,树就断成两块,正好是它的两棵子树:节点 2 领头那棵 3 个节点,节点 4 单独那棵 1 个节点。根节点比较特殊,它上面没有别的节点,所以没有上方那一块,下一帧确认一下。
- 12分数 = 3 × 1 = 3节点 0 的分数就是两棵子树大小相乘:3 乘 1 等于 3。它是第一个算的,先把当前最高分记成 3,达到最高分的节点数记成 1。别急着下结论,后面还有四个节点。
- 13正在删:节点 1轮到叶子节点 1。它没有孩子,删掉它以后,剩下的 4 个节点还连成一整块,不会散开。所以只裂出上方这一块,大小下一帧算。
- 14上方块 = 5 - 1 = 4再看上方那一块。它等于总数 n 减去节点 1 的子树大小,也就是 5 减 1 等于 4。这 4 个节点是删点之后除它子树以外剩下的全部,连在一起算一块。叶子就这一块。
- 15分数 = 4 = 4节点 1 的分数是各块大小相乘:4 = 4。这个 4 比之前的最高分 3 还大,所以刷新最高分为 4,并把达到最高分的节点数重新记成 1。注意叶子删完只剩一块,分数就等于那块的大小 4。
- 16正在删:节点 2轮到节点 2。删掉它和连着的边,先看它自己的子树:左孩子节点 1 那棵大小 1,右孩子节点 3 那棵大小 1。除了这两棵,它上面还连着一块,下一帧单独算。
- 17上方块 = 5 - 3 = 2再看上方那一块。它等于总数 n 减去节点 2 的子树大小,也就是 5 减 3 等于 2。这 2 个节点是删点之后除它子树以外剩下的全部,连在一起算一块。加上前面两棵子树,节点 2 删完一共 3 块。
- 18分数 = 1 × 1 × 2 = 2节点 2 的分数是 1 × 1 × 2 = 2。它比当前最高分 4 小,不是我们要的,最高分和计数都保持不变。可以看到节点 2 虽然裂成三块,乘积却只有 2,反而不占优。
- 19正在删:节点 3轮到叶子节点 3。它没有孩子,删掉它以后,剩下的 4 个节点还连成一整块,不会散开。所以只裂出上方这一块,大小下一帧算。
- 20上方块 = 5 - 1 = 4再看上方那一块。它等于总数 n 减去节点 3 的子树大小,也就是 5 减 1 等于 4。这 4 个节点是删点之后除它子树以外剩下的全部,连在一起算一块。叶子就这一块。
- 21分数 = 4 = 4节点 3 的分数是 4 = 4。它正好等于当前最高分 4,不刷新最高分,但达到最高分的节点数加一,变成 2。又一个叶子拿到 4 分。
- 22正在删:节点 4轮到叶子节点 4。它没有孩子,删掉它以后,剩下的 4 个节点还连成一整块,不会散开。所以只裂出上方这一块,大小下一帧算。
- 23上方块 = 5 - 1 = 4再看上方那一块。它等于总数 n 减去节点 4 的子树大小,也就是 5 减 1 等于 4。这 4 个节点是删点之后除它子树以外剩下的全部,连在一起算一块。叶子就这一块。
- 24分数 = 4 = 4节点 4 的分数是 4 = 4。它正好等于当前最高分 4,不刷新最高分,但达到最高分的节点数加一,变成 3。又一个叶子拿到 4 分。
- 25节点 1 / 3 / 4 都是 4停下来看个规律。三个叶子节点 1、3、4 的分数都是 4,也就是 n 减 1。原因很直接:删掉一个叶子,剩下的节点全都还连成一整块,大小就是 n 减 1,而这块又是唯一一块,乘积就是它自己。所以在很多树里,叶子的分数天然不低,常常就是那个最高分。
- 26答案 = 3五个节点全算完了。分数依次是节点 0 得 3、节点 1 得 4、节点 2 得 2、节点 3 得 4、节点 4 得 4。最高分是 4,达到它的有节点 1、节点 3、节点 4 三个,所以答案是 3。整个过程就是一趟后序求子树大小,再对每个节点把各块大小相乘比一比,时间是线性的。
⚠️ 容易写错的地方
✗ 错:用 int 存 score
✓ 对:C++ 用 long long、Java 用 long
分数是若干块大小的乘积,二叉树删点后最多三块,接近均分时最坏约 n 除以 3 的三次方、到 1e13 量级,32 位整型会溢出得到错误答案,64 位的 long 足够,Python 整数无上限不受影响
✗ 错:只把各棵子树大小相乘
✓ 对:还要乘上方那一块 n 减 size
删掉非根节点后,除了它的子树,上面还连着一整块,漏掉这块乘积就少一项、分数算小
✗ 错:给根节点也乘上方块
✓ 对:n 减 size 等于 0 时要跳过
根节点的子树就是全树,上方块大小是 0,乘进去分数直接变 0,所以只有这个差大于 0 才算一块
✗ 错:遇到更大分数只顾更新最高分
✓ 对:同时把计数重置为 1
出现新的最高分意味着之前的记录作废,计数要从 1 重新开始,之后相等才累加,否则数目会多算
完整代码(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 *
import sys
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
class Solution:
def countHighestScoreNodes(self, parents: List[int]) -> int:
# 链形树递归深度可达 n(最多十万),先调高递归上限,避免 RecursionError
sys.setrecursionlimit(200005)
def dfs(i: int, fa: int):
cnt = score = 1
for j in g[i]:
if j != fa:
t = dfs(j, i)
score *= t
cnt += t
if n - cnt:
score *= n - cnt
nonlocal ans, mx
if mx < score:
mx = score
ans = 1
elif mx == score:
ans += 1
return cnt
n = len(parents)
g = [[] for _ in range(n)]
for i in range(1, n):
g[parents[i]].append(i)
ans = mx = 0
dfs(0, -1)
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) {}
};
class Solution {
public:
int countHighestScoreNodes(vector<int>& parents) {
int n = parents.size();
vector<int> g[n];
for (int i = 1; i < n; ++i) {
g[parents[i]].push_back(i);
}
int ans = 0;
long long mx = 0;
function<int(int, int)> dfs = [&](int i, int fa) {
long long score = 1;
int cnt = 1;
for (int j : g[i]) {
if (j != fa) {
int t = dfs(j, i);
cnt += t;
score *= t;
}
}
if (n - cnt) {
score *= n - cnt;
}
if (mx < score) {
mx = score;
ans = 1;
} else if (mx == score) {
++ans;
}
return cnt;
};
dfs(0, -1);
return ans;
}
};Java
import java.util.*;
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 List<Integer>[] g;
private int ans;
private long mx;
private int n;
public int countHighestScoreNodes(int[] parents) {
n = parents.length;
g = new List[n];
Arrays.setAll(g, i -> new ArrayList<>());
for (int i = 1; i < n; ++i) {
g[parents[i]].add(i);
}
// 链形树递归深度可达 n(最多十万),默认线程栈会溢出;
// 把 DFS 放进一个更大栈的线程里跑,规避 StackOverflowError。
Thread worker = new Thread(null, () -> dfs(0, -1), "dfs", 1 << 26);
worker.start();
try {
worker.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return ans;
}
private int dfs(int i, int fa) {
int cnt = 1;
long score = 1;
for (int j : g[i]) {
if (j != fa) {
int t = dfs(j, i);
cnt += t;
score *= t;
}
}
if (n - cnt > 0) {
score *= n - cnt;
}
if (mx < score) {
mx = score;
ans = 1;
} else if (mx == score) {
++ans;
}
return cnt;
}
}复杂度
时间
O(n)
建孩子表扫一遍 parents 是 O(n);一趟 DFS 每个节点恰好访问一次,每次只做常数次乘法和比较。合起来随节点数线性增长
空间
O(n)
按峰值算。孩子表 g 存 n 减 1 条边,是 O(n);递归栈深度最坏是树退化成一条链时的 O(n)。两者都是 O(n),不额外开更大的表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 统计最高分的节点数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么一趟 DFS 就够,不用对每个节点单独重算?+
每个节点的分数只依赖两样东西:它每棵子树的大小,以及 n 减去它自己的子树大小。而所有子树大小,用一趟后序遍历就能全部求出。于是在这趟遍历里,回到每个节点时它孩子的子树大小已经就绪,顺手就能把分数乘出来并和最高分比较,总共只走一遍,时间 O(n)。
分数会不会溢出,怎么处理?+
会。分数是若干块大小的乘积,二叉树删掉一个节点后最多裂成三块,三块接近均分时每块约 n 的三分之一,乘起来最坏约 n 除以 3 的三次方,n 是十万时到约 3.7 乘 10 的 13 次方,也就是 1e13 量级,超过 32 位整型上限,但没到 64 位上限。所以 C plus plus 用 long long、Java 用 long 来存分数和当前最高分,Python 的整数没有上限,不用额外处理。
上方那一块到底指什么?+
把节点 i 和它的边删掉后,它的每棵子树各成一块,而它的父亲、祖先以及旁系分支这些节点会连成另一整块,这就是上方块。它的大小是 n 减去 size(i)。只有根节点例外,根的子树就是全树,上方块大小为 0,不计入。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 统计最高分的节点数目 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。