
1보다 큰 서로 다른 정수 배열 arr가 주어집니다. 각 숫자를 여러 번 써서 이진 트리를 만들 수 있는데, 리프가 아닌 노드의 값은 두 자식 값의 곱이어야 합니다. 만들 수 있는 트리의 개수를 10^9 + 7로 나눈 나머지를 구합니다.
Input: arr = [2,4,5,10]
Output: 7
// [2], [4], [5], [10], [4,2,2], [10,2,5], [10,5,2]
1 <= arr.length <= 10002 <= arr[i] <= 10^9내림차순으로 정렬하고, 각 값을 나눠 떨어지는 몫이 배열에 있으면 트리 하나를 센 뒤 왼쪽 자식으로 다시 내려가는 방식으로 풀었습니다.
function numFactoredBinaryTrees(arr: number[]): number {
const arrLen = arr.length
let trees = arrLen;
const sorted = arr.toSorted((a, b) => b - a);
const arrSet = new Set(arr);
function backTrack(i: number) {
for (let j = i + 1; j < arrLen; j++) {
const quot = sorted[i] / sorted[j];
if (arrSet.has(quot)) {
trees++;
backTrack(j);
}
}
}
for (let i = 0; i < arrLen; i++) backTrack(i);
return trees;
};
예제는 통과했지만 제출하면 틀립니다.
1. 경우의 수를 곱하지 않고 더했다
루트 x의 자식이 (a, b)라면 왼쪽 서브트리와 오른쪽 서브트리를 독립적으로 고를 수 있습니다. 그래서 그 루트로 만들 수 있는 트리 수는 (a의 트리 수) × (b의 트리 수)입니다.
첫 코드는 왼쪽 자식만 재귀로 펼쳤고, 오른쪽 자식(몫)은 항상 리프로 취급했습니다.
반례는 [2, 4, 16]입니다.
2: [2] → 1개4: [4], [4,2,2] → 2개16: 자식이 (4, 4)이므로 1 + 2 × 2 = 5개정답은 1 + 2 + 5 = 8인데, 첫 코드는 [16,4,[4,2,2]]와 [16,[4,2,2],[4,2,2]]를 빠뜨려 6을 반환합니다. 예제 2가 맞은 건 오른쪽 자식을 펼칠 일이 한 번도 없었기 때문입니다.
2. 메모이제이션이 없었다
같은 값의 서브트리를 부를 때마다 처음부터 다시 셉니다. 정답이 10^9 + 7로 나눠야 할 만큼 커질 수 있는데 1씩 올려서 세면 끝나지 않습니다.
3. 나머지 연산이 없었다
backTrack(i)가 전역 변수를 올리는 대신 sorted[i]를 루트로 하는 트리 수를 반환하게 바꿨습니다.
backTrack(i) = 1 + Σ backTrack(j) × backTrack(k)
(sorted[j] × sorted[k] = sorted[i])
function numFactoredBinaryTrees(arr: number[]): number {
const arrLen = arr.length
const MOD = BigInt(Math.pow(10, 9) + 7);
const sorted = arr.toSorted((a, b) => b - a);
const indexOf = new Map<number, number>(sorted.map((v, i) => [v, i]));
const memo: bigint[] = [];
function backTrack(i: number) {
if (memo[i]) return memo[i];
let trees = 1n;
for (let j = i + 1; j < arrLen; j++) {
const quot = sorted[i] / sorted[j];
const k = indexOf.get(quot);
if (k) trees += backTrack(j) * backTrack(k);
}
return memo[i] = trees % MOD;
}
let total = 0n;
for (let i = 0; i < arrLen; i++) total += backTrack(i);
return Number(total % MOD);
};
Set → Map: 몫의 트리 수까지 알아야 하니 값이 배열에 있는지만으로는 부족합니다. 값으로 인덱스를 찾을 수 있게 Map을 씁니다.[10,2,5]와 [10,5,2]는 다른 트리입니다. j가 2를 가리킬 때 한 번, 5를 가리킬 때 한 번 잡히므로 따로 처리하지 않아도 두 경우가 모두 세어집니다.10 / 4 = 2.5처럼 나눠 떨어지지 않으면 Map에 없으니 자연스럽게 건너뜁니다.if (k)가 안전한 이유: 인덱스 0은 falsy라 걸러지지만, 0번은 배열의 최댓값이고 몫은 항상 sorted[i]보다 작으므로 몫이 0번일 수 없습니다.10^18이라 Number.MAX_SAFE_INTEGER(약 9 × 10^15)를 넘습니다. 그래서 곱셈은 BigInt로 합니다.i를 한 번씩 계산하고, 그때마다 뒤쪽 원소를 훑습니다.memo와 indexOf를 씁니다. 재귀 깊이는 값이 매번 절반 이하로 줄어 최대 약 30입니다."찾을 때마다 +1"로 세면 덧셈만 남고, 독립적인 선택을 곱해야 한다는 점을 놓치기 쉽습니다. 각 값을 루트로 하는 개수를 반환하는 함수로 바꾸자 곱셈 구조와 메모이제이션이 자연스럽게 따라왔습니다.