带因子的二叉树 图解题解
这道题到底在问什么
- 输入
- arr = [2, 4, 5, 10]
- 输出
- 7
先想最直接的笨办法
记住这句口诀:排序、dp 从 1 起步、枚举因子对、乘法原理累加、最后求和。后面每一帧都在套它。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 823 带因子的二叉树:排序后 dp[v] 记以 v 为根的树数,枚举每对因子 a×b=v 累加 dp[a]×dp[b],答案是所有 dp 之和取模。命门在乘法组合,时间 O(n²)、空间 O(n)。
非叶结点等于两孩子相乘,这树怎么数
给一个数组 arr,元素互不相同、都大于 1。拿这些数当结点搭二叉树,同一个数可反复用,规矩是每个非叶结点必须正好等于左孩子乘右孩子。问能搭出多少种不同的树,对 1e9+7(一个常用大质数)取余。
拿题面的 arr = [2, 4, 5, 10],答案是 7:四个数各自单独成一棵只有根的树是 4 棵,再加 4 = 2×2、10 = 2×5、10 = 5×2 三棵有孩子的凑够 7(左 2 右 5 与左 5 右 2 算两棵)。
为什么不能把所有树形硬枚举一遍
一个数当根、两个孩子又各能往下长子树,形状随规模成倍翻,把每种树都摆出来一棵棵数根本数不完;而且同一棵子树在很多处被从头重算。既然「以某数为根能搭几棵树」能独立回答又反复用到,算一次存下来就行,就是动态规划那套。
为什么先排序,dp[v] 定成什么
先把 arr 从小到大排序。定义 dp[v] 为「以 v 为根能搭多少棵合法树」。v 当非叶根就得拆成两因子 a×b = v 当左右孩子,而 a、b 都大于 1,都严格小于 v。
a、b 都比 v 小,排完一定排在 v 左边,算 dp[v] 时它们的 dp 早已就绪;不排序会取到没累加完的半成品。
dp[a]×dp[b] 为什么是相乘不是相加
dp[v] 从 1 起步——单个数本身就是一棵只有根的合法树。再枚举每对满足 a×b = v 且 a、b 都在 arr 里的因子,把 dp[a]×dp[b] 累加进去。这里用乘法原理(两件事各自独立选,总方案是各自方案数相乘):左孩子有 dp[a] 种长法、右孩子有 dp[b] 种,各挑各的互不牵制,组合就是 dp[a]×dp[b] 种。
枚举只需扫一个因子 a,商 c = v÷a 若在 arr 里就配一对。累加是相乘不是相加,最易写反。
拿 [2,4,5,10] 把四个 dp 逐格填出来
排序后是 2, 4, 5, 10,dp 全从 1 起步。dp[2]:前面无更小的数,保持 1。dp[4]:先记单点 1 棵,试 2,4÷2 = 2 在表里,累加 dp[2]×dp[2] = 1,得 2。
dp[5]:试 2、试 4 都除不尽,锁定 1。dp[10]:先记单点 1 棵;试 2,10÷2 = 5 在表里,累加 dp[2]×dp[5] = 1,变 2;试 5,10÷5 = 2 在表里,累加 dp[5]×dp[2] = 1,变 3。四格是 1、2、1、3,答案 1+2+1+3 = 7,与题面对上。
dp[a]×dp[b] 不取余为什么会溢出成负数
外层枚举根、内层枚举较小因子,两层都是 n;配「值到下标」哈希表(一种能瞬间查『某个数在不在』的表)让「商在不在数组」做到 O(1),整体 O(n²),空间 O(n)。arr 全是质数或两两互质(任意两个数没有大于 1 的公因子)时,谁都拆不出表里的因子,答案就等于元素个数。
累加时 dp[a]×dp[b] 会撑爆整型,每加不立刻对 1e9+7 取余就溢出翻成负数、答案全错;a≠c 那对左右不对称,「左 a 右 c」和「左 c 右 a」是两棵不同的树,内层扫到 a 和扫到 c 各计一次,少数一遍就漏掉一半组合。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这句口诀:排序、dp 从 1 起步、枚举因子对、乘法原理累加、最后求和。后面每一帧都在套它。
- 4第一步先排序:2, 4, 5, 10。排序是为了保证算到 x 时,比它小的因子都已经算好 dp 了。右边面板就是 dp 表,现在每个值都是 1。
- 5dp[x] 为什么从 1 起步?因为任何一个数单独拎出来就是一棵只有根的合法树。这 4 个 1,就是示例 7 棵里最朴素的 4 棵。
- 6强调一下顺序:因子 b、c 都严格小于它们的乘积 x,排序后它们一定排在 x 前面、dp 已经算好。所以从左往右一个个算,永远能拿到现成的因子 dp。
- 7先看最小的 2。它要拆成两个因子相乘,最小也得是别的数当孩子,但没有比它更小的数在表里,所以 2 只能是单结点,dp[2] 保持 1。
- 8轮到 4。先把它自己当单结点那 1 棵记上,dp[4] 从 1 起步。接着去前面找:有没有两个数相乘正好等于 4,能当它的左右孩子。
- 9试因子 2:4 能被 2 整除,商是 2,而 2 也在表里。这就凑成一对合法孩子,左孩子 2、右孩子 2 相乘正好 4。
- 10用乘法原理:左子树有 dp[2] = 1 种长法,右子树有 dp[2] = 1 种,自由组合就是 1 × 1 = 1 种,累加进 dp[4],现在是 2。
- 114 的因子对枚举完了,dp[4] 最终是 2:单结点 1 棵,加上各因子对带来的组合。
- 12轮到 5。先把它自己当单结点那 1 棵记上,dp[5] 从 1 起步。接着去前面找:有没有两个数相乘正好等于 5,能当它的左右孩子。
- 13试因子 2:5 除以 2 除不尽,2 根本不是 5 的因子,当不了它的孩子,跳过。
- 14试因子 4:5 除以 4 除不尽,4 根本不是 5 的因子,当不了它的孩子,跳过。
- 155 前面的数都没法两两相乘等于它,所以它只有单结点这一种,dp[5] 锁定为 1。
- 16轮到 10。先把它自己当单结点那 1 棵记上,dp[10] 从 1 起步。接着去前面找:有没有两个数相乘正好等于 10,能当它的左右孩子。
- 17试因子 2:10 能被 2 整除,商是 5,而 5 也在表里。这就凑成一对合法孩子,左孩子 2、右孩子 5 相乘正好 10。
- 18用乘法原理:左子树有 dp[2] = 1 种长法,右子树有 dp[5] = 1 种,自由组合就是 1 × 1 = 1 种,累加进 dp[10],现在是 2。
- 19试因子 4:10 除以 4 除不尽,4 根本不是 10 的因子,当不了它的孩子,跳过。
- 20试因子 5:10 能被 5 整除,商是 2,而 2 也在表里。这就凑成一对合法孩子,左孩子 5、右孩子 2 相乘正好 10。
- 21用乘法原理:左子树有 dp[5] = 1 种长法,右子树有 dp[2] = 1 种,自由组合就是 1 × 1 = 1 种,累加进 dp[10],现在是 3。
- 2210 的因子对枚举完了,dp[10] 最终是 3:单结点 1 棵,加上各因子对带来的组合。
- 23四个 dp 都算好了:2 是 1 棵,4 是 2 棵,5 是 1 棵,10 是 3 棵。每个数当根能搭多少树,面板里一目了然。
- 24每个数都可以当整棵树的根,所以答案是把所有 dp 加起来:1 + 2 + 1 + 3 = 7。
- 25于是 arr = [2, 4, 5, 10] 一共能搭出 7 棵合法二叉树,和示例对上了。数据大时记得每步对 1e9+7 取余。
⚠️ 容易写错的地方
✗ 错:不排序就直接算
✓ 对:先从小到大排序
要保证算 dp[x] 时因子的 dp 已经算好,否则取到的是没累加完的值
✗ 错:累加用加法 dp[b] + dp[c]
✓ 对:乘法原理 dp[b] × dp[c]
左右子树独立组合,是相乘不是相加
✗ 错:只算一半忘了对称
✓ 对:枚举到的每个 (b, c) 都算,b 和 c 不同时是两种不同的树
左孩子 b 右孩子 c 与左 c 右 b 是不同的树,枚举因子时自然会各数一次
✗ 错:忘了取余或只在最后取一次
✓ 对:每次累加都对 1e9+7 取余
中间乘积会爆 long,必须边乘边取余
完整代码(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 Solution:
def numFactoredBinaryTrees(self, arr: List[int]) -> int:
mod = 10**9 + 7
n = len(arr)
arr.sort()
idx = {v: i for i, v in enumerate(arr)}
f = [1] * n
for i, a in enumerate(arr):
for j in range(i):
b = arr[j]
if a % b == 0 and (c := (a // b)) in idx:
f[i] = (f[i] + f[j] * f[idx[c]]) % mod
return sum(f) % modC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#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;
class Solution {
public:
int numFactoredBinaryTrees(vector<int>& arr) {
const int mod = 1e9 + 7;
sort(arr.begin(), arr.end());
unordered_map<int, int> idx;
int n = arr.size();
for (int i = 0; i < n; ++i) {
idx[arr[i]] = i;
}
vector<long> f(n, 1);
for (int i = 0; i < n; ++i) {
int a = arr[i];
for (int j = 0; j < i; ++j) {
int b = arr[j];
if (a % b == 0) {
int c = a / b;
if (idx.count(c)) {
int k = idx[c];
f[i] = (f[i] + 1l * f[j] * f[k]) % mod;
}
}
}
}
long ans = 0;
for (long v : f) {
ans = (ans + v) % mod;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int numFactoredBinaryTrees(int[] arr) {
final int mod = (int) 1e9 + 7;
Arrays.sort(arr);
int n = arr.length;
long[] f = new long[n];
Arrays.fill(f, 1);
Map<Integer, Integer> idx = new HashMap<>(n);
for (int i = 0; i < n; ++i) {
idx.put(arr[i], i);
}
for (int i = 0; i < n; ++i) {
int a = arr[i];
for (int j = 0; j < i; ++j) {
int b = arr[j];
if (a % b == 0) {
int c = a / b;
if (idx.containsKey(c)) {
int k = idx.get(c);
f[i] = (f[i] + f[j] * f[k]) % mod;
}
}
}
}
long ans = 0;
for (long v : f) {
ans = (ans + v) % mod;
}
return (int) ans;
}
}复杂度
时间
O(n²)
双层枚举根与因子,哈希查商 O(1)
空间
O(n)
dp 数组 + 值到下标的哈希表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 带因子的二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么只枚举一个因子 a 就够,不会漏掉 (c, a) 这种对称的树?+
因为内层会从头扫到根 v 前面的每一个数。当 a 当左孩子、c = v÷a 当右孩子时数一次;等内层扫到 c,c 当左孩子、a 当右孩子又数一次。左右两种排布本就是两棵不同的树,扫一整趟自然各计一遍,既不漏也不重。只有 a = c(比如 4 = 2×2)时左右一样,只此一棵,也只被数到一次。
dp[v] 为什么要从 1 起步,而不是从 0?+
那个 1 代表 v 单独成一棵只有根、没有孩子的树,这本身就是一种合法搭法,必须算进去。如果从 0 起步,就漏掉了所有单点树——本例 [2,4,5,10] 光单点就有 4 棵,答案会从 7 掉到 3。枚举因子对累加的是「v 当非叶根」的情形,和「v 当单点」这 1 棵互不重叠,加起来才是 dp[v] 的全部。
这题的转移和普通线性 dp 有什么不一样?+
普通线性 dp 的转移往往只盯着相邻或固定几个前驱,比如 dp[i] 只看 dp[i-1]、dp[i-2]。这里 dp[v] 依赖的是「所有能相乘得到 v 的因子对」,依赖项不固定、散落在前面各处,得靠哈希表 O(1) 判断某个商是否在数组里,把内层查找从 O(n) 压下来,整体停在 O(n²)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 带因子的二叉树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。