BINARY SEARCH TREE › VALIDATION AND PROPERTY
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →A BST containing values 1 through n has one root. If the root is j, every value smaller than j must appear in the left subtree, and every value larger than j must appear in the right subtree. The left side contains j - 1 values, while the right side contains n - j values, so choosing root j creates two independent smaller BST problems.
The number of trees using root j is the number of possible left subtrees multiplied by the number of possible right subtrees. Multiplication is valid because every left choice can be paired with every right choice. Summing that product over all root values counts every structurally unique tree exactly once, because each tree has one and only one root.
That recurrence depends only on the number of values in a subtree, not on their actual labels. Let dp[i] be the number of structurally unique BSTs containing any i consecutive values. Then dp[i] can be built from already known values dp[0] through dp[i - 1]. The empty subtree contributes one possibility, which is why dp[0] starts at 1.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(n^2) | There are n - 1 subtree sizes that need work, and size i examines i possible roots. Each transition performs constant-time arithmetic, so the total number of transitions is 1 + 2 + ... + n - 1, which is O(n^2). |
| Space | O(n) extra | The dp array stores one count for each size from 0 through n. The returned count is a scalar output, so it is not counted as working memory. This bound does not degrade with any tree shape because the algorithm constructs no tree and depends only on n. |
#include <vector>
using namespace std;
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n + 1, 0);
dp[0] = dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
};The expression dp[j - 1] multiplied by dp[i - j] is the entire structural argument in code. The root itself uses one value, leaving j - 1 values on its left and i - j values on its right. The loops visit each possible root exactly once, so no tree is duplicated and no root split is omitted.
The DP values are Catalan numbers, and consecutive Catalan numbers have a direct multiplicative relationship. Starting with C0 = 1, compute the next value with Ck = Ck-1 multiplied by 2(2k - 1), divided by k + 1. With n at most 19, a long long safely holds the intermediate arithmetic, while the final answer fits the required int return type.
#include <vector>
using namespace std;
class Solution {
public:
int numTrees(int n) {
long long trees = 1;
for (int k = 1; k <= n; k++) {
trees = trees * 2 * (2 * k - 1) / (k + 1);
}
return static_cast<int>(trees);
}
};This is an optimization, not merely a rearrangement: it changes the time bound from O(n^2) to O(n) and the extra space from O(n) to O(1). It buys a shorter state footprint but hides the root-splitting proof inside the Catalan identity. The DP version is usually easier to derive from the problem, while the formula is reasonable when the Catalan connection is familiar.