Facebook Pixel

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 with 2 as root, 1 as left child, and 3 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:

  1. Separates elements into left subtree (values < root) and right subtree (values > root)
  2. Calculates the number of valid orderings for each subtree recursively
  3. Uses combinations C(m+n, m) to determine how many ways to interleave m left elements with n right elements while maintaining their relative orders
  4. 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."

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. The relative order among left elements produces the correct left subtree
  2. 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 all i (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:

  1. Base Case: If the array has less than 2 elements, return 1 (only one way to arrange)

  2. Partition: Split the array based on the first element (root):

    • left = [x for x in nums if x < nums[0]] - elements going to left subtree
    • right = [x for x in nums if x > nums[0]] - elements going to right subtree
  3. Recursive Calls: Calculate valid arrangements for each subtree:

    • a = dfs(left) - number of valid arrangements for left subtree
    • b = dfs(right) - number of valid arrangements for right subtree
  4. Combine Results: Use the formula:

    result = C(m+n, m) × a × b

    where m = len(left) and n = len(right)

The multiplication C(m+n, m) × a × b represents:

  • C(m+n, m): ways to interleave left and right elements
  • a: valid internal arrangements of left subtree
  • b: 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 Evaluator

Example 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:

  1. [3, 1, 2, 4] - original order
  2. [3, 1, 4, 2] - insert 4 before 2 (both go right of 1 anyway)
  3. [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:

  1. Preprocessing the combination table: The nested loops to build the combination table c take O(n²) time, where we compute C(i,j) for all valid i and j values up to n.

  2. 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.

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:

  1. Combination table: The 2D array c of size n × n requires O(n²) space to store all the binomial coefficients.

  2. Recursion stack: In the worst case (skewed tree), the recursion depth can be O(n).

  3. Temporary arrays: At each recursive level, new arrays left and right are created. In total across all recursive calls, this uses O(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]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More