带因子的二叉树 图解题解
这道题到底在问什么
- 输入
- arr = [2, 4, 5, 10]
- 输出
- 7
先想最直接的笨办法
记住这句口诀:排序、dp 从 1 起步、枚举因子对、乘法原理累加、最后求和。后面每一帧都在套它。(动画第 3 步)
最优解:一步一步想明白
- 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++ 跟着练。
面试官可能追问
为什么枚举一个因子 b 就够,不会漏掉 (c, b) 这种对称情况?+
因为内层 j 会从头扫到 i 前面所有的数。当 b 当左孩子时算一次,等 j 扫到 c 时,c 当左孩子、b 当右孩子又算一次,两种左右排布天然各数到一遍,所以不会漏也不会重。
这题和普通的线性 dp 有什么不同?+
普通 dp 的转移往往只看相邻或固定几个前驱,这里的转移要枚举所有因子对,依赖项不固定,靠哈希表 O(1) 判断「商在不在数组」把内层从 O(n) 的查找降下来,整体 O(n²)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 带因子的二叉树 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。