1569. Number of Ways to Reorder Array to Get Same BST
Problem Description
You are given an array nums
that represents a permutation of integers from 1
to n
. When you insert the elements of nums
in order into an initially empty Binary Search Tree (BST), you get a specific tree structure.
The task is to find how many different ways you can reorder the elements in nums
such that when you insert them into an empty BST in the new order, you get the exact same BST structure as the original.
For example, with nums = [2,1,3]
:
- Inserting in order
[2,1,3]
creates a BST with2
as root,1
as left child, and3
as right child - The reordering
[2,3,1]
produces the same BST structure (insert 2 first as root, then 3 goes right, then 1 goes left) - But
[3,2,1]
produces a different BST (3 becomes root instead of 2)
The key insight is that to maintain the same BST structure:
- The first element must remain the same (it's always the root)
- All elements less than the root must go to the left subtree
- All elements greater than the root must go to the right subtree
- The relative insertion order within left and right subtrees must preserve their respective BST structures
Since the answer can be very large, return it modulo 10^9 + 7
.
The solution uses a recursive approach with combinatorics. For any given array, it:
- Separates elements into left subtree (values < root) and right subtree (values > root)
- Calculates the number of valid orderings for each subtree recursively
- Uses combinations
C(m+n, m)
to determine how many ways to interleavem
left elements withn
right elements while maintaining their relative orders - Multiplies these values together:
C(m+n, m) × ways(left) × ways(right)
The final answer subtracts 1 because the original ordering is not considered a "reordering."
Intuition
To understand why certain reorderings produce the same BST, let's think about what determines a BST's structure. When we insert elements sequentially, each element finds its position by comparing with existing nodes - going left if smaller, right if larger. The crucial observation is that the first element always becomes the root, and this root permanently divides all subsequent elements into two groups: those that will go to the left subtree (smaller values) and those that will go to the right subtree (larger values).
Once we insert the root, it doesn't matter in what order we insert elements 1
and 3
after 2
in the example [2,1,3]
- element 1
will always end up as the left child and 3
as the right child. This is because their final positions are determined solely by their values relative to 2
.
This leads to a key insight: we can recursively break down the problem. For any array with root r
, we can identify:
- Left elements: all values <
r
- Right elements: all values >
r
Now, how many valid orderings exist? We need the root first, but after that, we can interleave the left and right elements in any way, as long as:
- The relative order among left elements produces the correct left subtree
- The relative order among right elements produces the correct right subtree
If we have m
elements for the left subtree and n
for the right, we're essentially choosing m
positions out of m+n
total positions for the left elements. This is the combination C(m+n, m)
. The remaining positions automatically go to the right elements.
For example, with left = [1]
and right = [3,4]
, we can arrange them as:
[1,3,4]
- choosing position 0 for left[3,1,4]
- choosing position 1 for left[3,4,1]
- choosing position 2 for left
That's C(3,1) = 3
ways.
The total number of valid arrangements becomes:
C(m+n, m) × (valid arrangements of left subtree) × (valid arrangements of right subtree)
We apply this formula recursively until we reach base cases (arrays with 0 or 1 element have exactly 1 arrangement). The final answer subtracts 1 because we want reorderings different from the original.
Learn more about Tree, Union Find, Binary Search Tree, Memoization, Math, Divide and Conquer, Dynamic Programming, Binary Tree and Combinatorics patterns.
Solution Approach
The implementation consists of two main components: preprocessing combinations and a recursive DFS function.
Step 1: Preprocessing Combinations Table
First, we build a 2D array c
where c[i][j]
represents C(i, j)
- the number of ways to choose j
items from i
items. We use Pascal's triangle formula:
- Base case:
c[i][0] = 1
for alli
(choosing 0 items has 1 way) - Recurrence:
c[i][j] = c[i-1][j] + c[i-1][j-1]
This preprocessing allows us to look up combination values in O(1) time during recursion.
c = [[0] * n for _ in range(n)]
c[0][0] = 1
for i in range(1, n):
c[i][0] = 1
for j in range(1, i + 1):
c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod
Step 2: Recursive DFS Function
The dfs(nums)
function calculates the number of valid arrangements for array nums
:
-
Base Case: If the array has less than 2 elements, return 1 (only one way to arrange)
-
Partition: Split the array based on the first element (root):
left = [x for x in nums if x < nums[0]]
- elements going to left subtreeright = [x for x in nums if x > nums[0]]
- elements going to right subtree
-
Recursive Calls: Calculate valid arrangements for each subtree:
a = dfs(left)
- number of valid arrangements for left subtreeb = dfs(right)
- number of valid arrangements for right subtree
-
Combine Results: Use the formula:
result = C(m+n, m) × a × b
where
m = len(left)
andn = len(right)
The multiplication C(m+n, m) × a × b
represents:
C(m+n, m)
: ways to interleave left and right elementsa
: valid internal arrangements of left subtreeb
: valid internal arrangements of right subtree
Step 3: Final Answer
The function dfs(nums)
returns the total number of ways to arrange elements (including the original). Since we want only the reorderings (excluding the original), we subtract 1:
return (dfs(nums) - 1 + mod) % mod
The + mod
before the final modulo ensures the result remains positive even after subtraction.
Time Complexity: O(n²) where n is the length of the array - we visit each element once in recursion, and for each recursive call, we partition the array in O(n) time.
Space Complexity: O(n²) for the combination table plus O(n) for recursion stack depth.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [3, 1, 2, 4]
.
Step 1: Build Combinations Table We create a table for C(n,k) values. For n=4, we need:
- C(0,0) = 1
- C(1,0) = 1, C(1,1) = 1
- C(2,0) = 1, C(2,1) = 2, C(2,2) = 1
- C(3,0) = 1, C(3,1) = 3, C(3,2) = 3, C(3,3) = 1
Step 2: Process dfs([3, 1, 2, 4])
- Root = 3 (first element)
- Partition around 3:
- left = [1, 2] (elements < 3)
- right = [4] (elements > 3)
- Need to calculate: C(2+1, 2) × dfs([1,2]) × dfs([4])
Step 3: Process dfs([1, 2])
- Root = 1
- Partition around 1:
- left = [] (no elements < 1)
- right = [2] (elements > 1)
- Calculate: C(0+1, 0) × dfs([]) × dfs([2])
- dfs([]) = 1 (base case)
- dfs([2]) = 1 (single element)
- Result: C(1,0) × 1 × 1 = 1 × 1 × 1 = 1
Step 4: Process dfs([4])
- Single element, return 1
Step 5: Combine Results for Original Array
- dfs([3,1,2,4]) = C(3,2) × dfs([1,2]) × dfs([4])
- = 3 × 1 × 1 = 3
This means there are 3 total ways to arrange [3,1,2,4] that produce the same BST:
- [3, 1, 2, 4] - original order
- [3, 1, 4, 2] - insert 4 before 2 (both go right of 1 anyway)
- [3, 4, 1, 2] - insert 4 before both 1 and 2
Let's verify [3, 4, 1, 2] produces the same BST:
- Insert 3: becomes root
- Insert 4: 4 > 3, goes right of 3
- Insert 1: 1 < 3, goes left of 3
- Insert 2: 2 < 3 (go left), 2 > 1 (go right of 1)
This creates the same structure as the original!
Step 6: Final Answer Total arrangements - 1 (excluding original) = 3 - 1 = 2
The answer is 2 different ways to reorder the array.
Solution Implementation
1class Solution:
2 def numOfWays(self, nums: List[int]) -> int:
3 def count_bst_permutations(arr):
4 """
5 Count the number of permutations that produce the same BST structure.
6
7 Args:
8 arr: Current array/subtree to process
9
10 Returns:
11 Number of valid permutations for this subtree
12 """
13 # Base case: arrays with less than 2 elements have only 1 permutation
14 if len(arr) < 2:
15 return 1
16
17 # Split array into left and right subtrees based on first element (root)
18 root = arr[0]
19 left_subtree = [x for x in arr if x < root]
20 right_subtree = [x for x in arr if x > root]
21
22 # Get sizes of left and right subtrees
23 left_size = len(left_subtree)
24 right_size = len(right_subtree)
25
26 # Recursively count permutations for left and right subtrees
27 left_permutations = count_bst_permutations(left_subtree)
28 right_permutations = count_bst_permutations(right_subtree)
29
30 # Calculate total permutations using combinations
31 # C(left_size + right_size, left_size) represents ways to interleave
32 # left and right subtree elements while maintaining their relative order
33 total_size = left_size + right_size
34 combinations = binomial_coefficients[total_size][left_size]
35
36 # Total permutations = combinations * left_permutations * right_permutations
37 result = (combinations * left_permutations % MOD) * right_permutations % MOD
38 return result
39
40 # Initialize constants
41 n = len(nums)
42 MOD = 10**9 + 7
43
44 # Precompute binomial coefficients using Pascal's triangle
45 # binomial_coefficients[i][j] represents C(i, j) = i! / (j! * (i-j)!)
46 binomial_coefficients = [[0] * n for _ in range(n)]
47 binomial_coefficients[0][0] = 1
48
49 for i in range(1, n):
50 binomial_coefficients[i][0] = 1 # C(i, 0) = 1
51 for j in range(1, i + 1):
52 # Pascal's triangle formula: C(i, j) = C(i-1, j) + C(i-1, j-1)
53 binomial_coefficients[i][j] = (
54 binomial_coefficients[i - 1][j] +
55 binomial_coefficients[i - 1][j - 1]
56 ) % MOD
57
58 # Calculate total permutations and subtract 1 (excluding original arrangement)
59 total_permutations = count_bst_permutations(nums)
60 return (total_permutations - 1 + MOD) % MOD
61
1class Solution {
2 // 2D array to store binomial coefficients (Pascal's triangle)
3 private int[][] binomialCoefficients;
4 private final int MOD = (int) 1e9 + 7;
5
6 public int numOfWays(int[] nums) {
7 int n = nums.length;
8
9 // Precompute binomial coefficients using Pascal's triangle
10 // binomialCoefficients[i][j] represents C(i, j) = i choose j
11 binomialCoefficients = new int[n][n];
12 binomialCoefficients[0][0] = 1;
13
14 for (int i = 1; i < n; ++i) {
15 binomialCoefficients[i][0] = 1; // C(i, 0) = 1
16 for (int j = 1; j <= i; ++j) {
17 // Pascal's triangle formula: C(i, j) = C(i-1, j) + C(i-1, j-1)
18 binomialCoefficients[i][j] = (binomialCoefficients[i - 1][j] +
19 binomialCoefficients[i - 1][j - 1]) % MOD;
20 }
21 }
22
23 // Convert array to list for easier manipulation in recursion
24 List<Integer> numbersList = new ArrayList<>();
25 for (int num : nums) {
26 numbersList.add(num);
27 }
28
29 // Calculate total ways and subtract 1 (the original sequence itself)
30 return (dfs(numbersList) - 1 + MOD) % MOD;
31 }
32
33 /**
34 * Recursively calculates the number of ways to reorder the BST sequence
35 * @param nums Current list of numbers to process
36 * @return Number of valid BST sequences that can be formed
37 */
38 private int dfs(List<Integer> nums) {
39 // Base case: empty list or single element has only 1 way to arrange
40 if (nums.size() < 2) {
41 return 1;
42 }
43
44 // The first element will be the root of the BST
45 int root = nums.get(0);
46
47 // Partition elements into left and right subtrees
48 List<Integer> leftSubtree = new ArrayList<>();
49 List<Integer> rightSubtree = new ArrayList<>();
50
51 for (int num : nums) {
52 if (num < root) {
53 leftSubtree.add(num); // Elements less than root go to left subtree
54 } else if (num > root) {
55 rightSubtree.add(num); // Elements greater than root go to right subtree
56 }
57 // Skip the root itself (num == root case)
58 }
59
60 // Get sizes of left and right subtrees
61 int leftSize = leftSubtree.size();
62 int rightSize = rightSubtree.size();
63
64 // Recursively calculate ways for left and right subtrees
65 int leftWays = dfs(leftSubtree);
66 int rightWays = dfs(rightSubtree);
67
68 // Total ways = leftWays * rightWays * C(leftSize + rightSize, rightSize)
69 // The binomial coefficient represents ways to interleave left and right sequences
70 return (int) ((long) leftWays * rightWays % MOD *
71 binomialCoefficients[leftSize + rightSize][rightSize] % MOD);
72 }
73}
74
1class Solution {
2public:
3 int numOfWays(vector<int>& nums) {
4 int n = nums.size();
5 const int MOD = 1e9 + 7;
6
7 // Build Pascal's triangle for combinatorial calculations
8 // pascal[i][j] represents C(i, j) = number of ways to choose j items from i items
9 vector<vector<int>> pascal(n, vector<int>(n, 0));
10 pascal[0][0] = 1;
11
12 for (int i = 1; i < n; ++i) {
13 pascal[i][0] = 1; // C(i, 0) = 1
14 for (int j = 1; j <= i; ++j) {
15 // C(i, j) = C(i-1, j) + C(i-1, j-1)
16 pascal[i][j] = (pascal[i - 1][j] + pascal[i - 1][j - 1]) % MOD;
17 }
18 }
19
20 // Recursive function to count the number of valid BST arrangements
21 function<int(vector<int>)> countBSTArrangements = [&](vector<int> sequence) -> int {
22 // Base case: empty or single element has only one arrangement
23 if (sequence.size() < 2) {
24 return 1;
25 }
26
27 // Partition elements based on the root (first element)
28 vector<int> leftSubtree, rightSubtree;
29 int root = sequence[0];
30
31 for (int& value : sequence) {
32 if (value < root) {
33 leftSubtree.push_back(value); // Elements smaller than root go to left
34 } else if (value > root) {
35 rightSubtree.push_back(value); // Elements larger than root go to right
36 }
37 }
38
39 int leftSize = leftSubtree.size();
40 int rightSize = rightSubtree.size();
41
42 // Recursively count arrangements for left and right subtrees
43 int leftArrangements = countBSTArrangements(leftSubtree);
44 int rightArrangements = countBSTArrangements(rightSubtree);
45
46 // Total arrangements = C(leftSize + rightSize, leftSize) * leftArrangements * rightArrangements
47 // This represents choosing positions for left subtree elements among all positions
48 return (1LL * pascal[leftSize + rightSize][leftSize] * leftArrangements % MOD) * rightArrangements % MOD;
49 };
50
51 // Subtract 1 because we don't count the original arrangement
52 return (countBSTArrangements(nums) - 1 + MOD) % MOD;
53 }
54};
55
1/**
2 * Calculate the number of different ways to reorder nums to get the same BST
3 * @param nums - Array of distinct integers representing BST insertion order
4 * @returns Number of reorderings that produce the same BST (excluding original)
5 */
6function numOfWays(nums: number[]): number {
7 const arrayLength: number = nums.length;
8 const MOD: number = 1e9 + 7;
9
10 // Build Pascal's triangle for combination calculations
11 // combinationTable[n][k] represents C(n, k) = n! / (k! * (n-k)!)
12 const combinationTable: number[][] = new Array(arrayLength)
13 .fill(0)
14 .map(() => new Array(arrayLength).fill(0));
15
16 // Initialize Pascal's triangle
17 combinationTable[0][0] = 1;
18 for (let row = 1; row < arrayLength; ++row) {
19 combinationTable[row][0] = 1; // C(n, 0) = 1
20 for (let col = 1; col <= row; ++col) {
21 // C(n, k) = C(n-1, k-1) + C(n-1, k)
22 combinationTable[row][col] = (combinationTable[row - 1][col - 1] +
23 combinationTable[row - 1][col]) % MOD;
24 }
25 }
26
27 /**
28 * Recursively calculate number of valid BST arrangements
29 * @param currentNums - Current array segment to process
30 * @returns Number of valid arrangements for this subtree
31 */
32 const calculateArrangements = (currentNums: number[]): number => {
33 // Base case: empty or single element has only one arrangement
34 if (currentNums.length < 2) {
35 return 1;
36 }
37
38 // Split array into left and right subtrees based on root (first element)
39 const leftSubtree: number[] = [];
40 const rightSubtree: number[] = [];
41 const rootValue: number = currentNums[0];
42
43 for (let i = 1; i < currentNums.length; ++i) {
44 if (currentNums[i] < rootValue) {
45 leftSubtree.push(currentNums[i]);
46 } else {
47 rightSubtree.push(currentNums[i]);
48 }
49 }
50
51 // Get sizes and calculate arrangements for subtrees
52 const leftSize: number = leftSubtree.length;
53 const rightSize: number = rightSubtree.length;
54 const leftArrangements: number = calculateArrangements(leftSubtree);
55 const rightArrangements: number = calculateArrangements(rightSubtree);
56
57 // Calculate total arrangements using combination formula
58 // We choose leftSize positions from (leftSize + rightSize) for left subtree elements
59 // The remaining positions go to right subtree elements
60 const combinations: bigint = BigInt(combinationTable[leftSize + rightSize][leftSize]);
61 const leftArrangementsBig: bigint = BigInt(leftArrangements);
62 const rightArrangementsBig: bigint = BigInt(rightArrangements);
63
64 return Number((combinations * leftArrangementsBig * rightArrangementsBig) % BigInt(MOD));
65 };
66
67 // Calculate total arrangements and subtract 1 for the original arrangement
68 return (calculateArrangements(nums) - 1 + MOD) % MOD;
69}
70
Time and Space Complexity
Time Complexity: O(n²)
The time complexity analysis consists of two main parts:
-
Preprocessing the combination table: The nested loops to build the combination table
c
takeO(n²)
time, where we computeC(i,j)
for all validi
andj
values up ton
. -
DFS traversal:
- Each recursive call processes each element in the input array exactly once to partition it into left and right subarrays (using list comprehensions to filter elements less than and greater than
nums[0]
). - Across all recursive calls, every element is visited and processed
O(n)
times in total (once at each level it appears). - The partitioning at each level takes linear time relative to the size of that subarray.
- Overall, this contributes
O(n²)
time in the worst case.
- Each recursive call processes each element in the input array exactly once to partition it into left and right subarrays (using list comprehensions to filter elements less than and greater than
The dominant operation is O(n²)
from both the preprocessing and the recursive processing.
Space Complexity: O(n²)
The space complexity breaks down as follows:
-
Combination table: The 2D array
c
of sizen × n
requiresO(n²)
space to store all the binomial coefficients. -
Recursion stack: In the worst case (skewed tree), the recursion depth can be
O(n)
. -
Temporary arrays: At each recursive level, new arrays
left
andright
are created. In total across all recursive calls, this usesO(n)
space since each element appears in exactly one array at each recursion level.
The dominant space usage is the O(n²)
combination table, making the overall space complexity O(n²)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Modulo Operation on Subtraction
Pitfall: When subtracting 1 from the result to exclude the original arrangement, directly doing (dfs(nums) - 1) % MOD
can produce negative results if dfs(nums)
returns 0 or 1.
Why it happens: In modular arithmetic, if dfs(nums) % MOD
equals 0, then (0 - 1) % MOD
gives -1 in many programming languages, not the expected MOD - 1
.
Solution: Always add MOD
before taking the final modulo:
return (total_permutations - 1 + MOD) % MOD
2. Integer Overflow in Multiplication
Pitfall: Computing combinations * left_permutations * right_permutations
without intermediate modulo operations can cause integer overflow even in Python (though Python handles big integers, it's inefficient).
Why it happens: The multiplication of three potentially large numbers can exceed typical integer limits before the modulo operation is applied.
Solution: Apply modulo after each multiplication:
# Wrong - can overflow or be inefficient result = (combinations * left_permutations * right_permutations) % MOD # Correct - prevents overflow result = (combinations * left_permutations % MOD) * right_permutations % MOD
3. Off-by-One Error in Combinations Table
Pitfall: Incorrectly sizing the combinations table or using wrong indices when accessing it.
Why it happens: The combinations table needs to handle cases where we might need C(n-1, k)
where n
is the array length. If the table is sized as [n-1][n-1]
, it will cause index out of bounds errors.
Solution: Size the table as [n][n]
and ensure proper bounds in the loop:
# Correct sizing
binomial_coefficients = [[0] * n for _ in range(n)]
# Correct loop bounds
for i in range(1, n):
for j in range(1, i + 1): # j should not exceed i
4. Forgetting Base Cases
Pitfall: Not handling arrays with 0 or 1 elements properly in the recursive function.
Why it happens: The recursive logic assumes there's a root and possible left/right subtrees, which doesn't apply to empty or single-element arrays.
Solution: Always check for base cases at the beginning:
if len(arr) < 2: # Handles both empty and single-element arrays
return 1
5. Incorrect Array Partitioning
Pitfall: Including the root element in left or right subtree partitions, or using wrong comparison operators.
Why it happens: Confusion about whether to use <
, <=
, >
, or >=
when partitioning around the root.
Solution: Use strict inequalities and skip the root element:
root = arr[0] left_subtree = [x for x in arr[1:] if x < root] # Note arr[1:] to skip root right_subtree = [x for x in arr[1:] if x > root] # Or iterate through all elements but skip when x equals root left_subtree = [x for x in arr if x < root] # This naturally excludes root right_subtree = [x for x in arr if x > root]
How many ways can you arrange the three letters A, B and C?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Binary Search Tree Intro Definition A Binary Search Tree or BST is a rooted binary tree with the value of each internal node being greater than all the values in the respective node's left subtree and less than the ones in its right subtree An empty tree is a BST since it satisfies the
Want a Structured Path to Master System Design Too? Don’t Miss This!