Unique Binary Search Trees
Catalan-number DP over root choice
Catalan numbers via DP. dp[n] = Σ dp[i-1] * dp[n-i].
Problem
Given an integer n, return the number of structurally distinct BSTs that store the values 1, 2, …, n.
Intuition
Pick a root. Say you root the tree at value i. Then:
- The left subtree must hold values
1..=i-1— that'si - 1values, arranged as a BST. - The right subtree must hold values
i+1..=n— that'sn - ivalues, arranged as a BST.
Crucially, the count of BSTs depends only on the size, not the specific values: dp[k] = "how many BSTs can you build with k distinct ordered values?" The root i splits n total values into i - 1 on the left and n - i on the right, and the two subtrees are independent. Multiply their counts; sum over all root choices:
dp[n] = Σ_{i=1..n} dp[i - 1] * dp[n - i]
Base: dp[0] = 1 (the empty tree counts as one structure) and dp[1] = 1.
This is the Catalan recurrence. The sequence 1, 1, 2, 5, 14, 42, 132, … counts dozens of combinatorial structures: balanced parentheses, non-crossing chord diagrams, monotonic lattice paths, full binary trees. The BST count is the tree-shaped instance of that family.
Approach
Catalan DP
O(n²)O(n)Fill dp[0..=n] forward. For each k, iterate i = 1..=k and accumulate dp[i - 1] * dp[k - i]. n² multiplications total; n = 19 fits in i32 (dp[19] = 1,767,263,190), n = 20 overflows — the LC constraint caps n at 19 for that reason.
impl Solution {
pub fn num_trees(n: i32) -> i32 {
let n = n as usize;
let mut dp = vec![0i64; n + 1];
dp[0] = 1;
for k in 1..=n {
for i in 1..=k {
dp[k] += dp[i - 1] * dp[k - i];
}
}
dp[n] as i32
}
}function numTrees(n: number): number {
const dp = new Array<number>(n + 1).fill(0);
dp[0] = 1;
for (let k = 1; k <= n; k++) {
for (let i= 1; i <= k; i++) {
dp[k] = dp[i - 1] * dp[k - i];
}
}
return dp[n];
}Rust uses i64 intermediates so the multiplications never overflow before the final cast. TypeScript's number is a 64-bit float with 53 bits of integer precision — plenty for this sequence.
Trace
Fill for n = 3:
| k | breakdown | dp[k] |
|---|---|---|
| 0 | base | 1 |
| 1 | dp[0]·dp[0] | 1 |
| 2 | dp[0]·dp[1] + dp[1]·dp[0] = 1 + 1 | 2 |
| 3 | dp[0]·dp[2] + dp[1]·dp[1] + dp[2]·dp[0] = 2 + 1 + 2 | 5 |
Return dp[3] = 5. ✓
Edge cases
n = 0— return1(the empty tree). The LC problem guaranteesn ≥ 1, but the DP handles it for free viadp[0] = 1.n = 19—1,767,263,190, still fits ini32.n = 20would overflow; LC bounds the input accordingly.- Large
noff-platform — switch toi64orBigInt, or use the closed form.
The closed form is C_n = C(2n, n) / (n + 1). For BST counting, that's technically faster (O(n) with modular inverse or O(1) with precomputed factorials), but introduces floating-point roundoff on LC inputs if you compute via f64. The O(n²) DP is fast enough at n ≤ 19 and has the pedagogical benefit of showing why the count is Catalan — the root-split derivation.
Takeaway
When a problem asks "how many structures of size n?" and you can split it by a pivot choice, the answer is often Catalan. The template is: fix a pivot, multiply independent sub-counts, sum over pivot choices. That's Catalan for BSTs (pivot = root), parentheses (pivot = matching brace), full binary trees (pivot = root), and non-crossing chords (pivot = the chord from vertex 1).
This is also your first interval-DP-adjacent problem. The recurrence "split a range into two halves, recurse on each" generalizes to #241 Different Ways to Add Parentheses (pivot = operator), #95 Unique BSTs II (this problem plus tree reconstruction), and the interval-DP workhorses in Stage 3's final track.
More in DP on Trees & Intervals
House Robber on a tree. Return (rob_this, skip_this) pair from each subtree.
Interval DP or greedy with monotonic stack. Try both!
Interval DP. The trick: think about which balloon you burst LAST, not first.
Interval DP. Sort cuts, then dp[i][j] = min cost to process segment.