Facebook Pixel

3109. Find the Index of Permutation 🔒

Problem Description

You are given an array perm of length n which is a permutation of [1, 2, ..., n]. Your task is to find the index (0-based) of this permutation in the lexicographically sorted list of all possible permutations of [1, 2, ..., n].

For example, if n = 3, all permutations in lexicographical order are:

  • Index 0: [1, 2, 3]
  • Index 1: [1, 3, 2]
  • Index 2: [2, 1, 3]
  • Index 3: [2, 3, 1]
  • Index 4: [3, 1, 2]
  • Index 5: [3, 2, 1]

The problem asks you to determine which position (index) the given permutation perm occupies in this sorted sequence.

Since the total number of permutations can be very large (n!), and consequently the index can also be very large, you need to return the result modulo 10^9 + 7.

The solution uses a Binary Indexed Tree (Fenwick Tree) to efficiently count how many permutations come before the given one. For each position i in the permutation, it calculates how many permutations would be lexicographically smaller by:

  1. Counting elements that could appear at position i and are smaller than perm[i]
  2. Multiplying this count by the factorial of remaining positions (n-i-1)!
  3. Using the Binary Indexed Tree to track which numbers have already been used in previous positions

The sum of all these contributions gives the final index of the permutation in the lexicographically sorted list.

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

Intuition

To find the index of a permutation, we need to count how many permutations come before it lexicographically. Let's think about this step by step.

Consider a permutation like [3, 1, 4, 2]. How many permutations are smaller than this one?

First, any permutation starting with 1 or 2 will be smaller than one starting with 3. If we fix the first position as 1, we can arrange the remaining numbers {2, 3, 4} in any order - that's 3! ways. Similarly for starting with 2. So permutations starting with numbers less than 3 contribute 2 × 3! to our count.

Now, for permutations starting with 3, we need to look at the second position. Our permutation has 1 in the second position. Among the remaining unused numbers {1, 2, 4}, there are no numbers smaller than 1, so no additional permutations are smaller at this level.

Moving to the third position with value 4, the remaining unused numbers are {2, 4}. The number 2 is smaller than 4, so any permutation with [3, 1, 2, ...] would be smaller than [3, 1, 4, ...]. That adds 1 × 1! more permutations.

This reveals the pattern: at each position i, we count how many unused numbers are smaller than perm[i], then multiply by (n-i-1)! (the factorial of remaining positions).

The key challenge is efficiently tracking which numbers have been used and counting how many available numbers are smaller than the current one. Initially, all numbers from 1 to n are available. As we process each position, we "remove" that number from our available set.

A Binary Indexed Tree perfectly solves this tracking problem. We can:

  • Query how many numbers up to x have been used: tree.query(x)
  • Mark a number as used: tree.update(x, 1)
  • Calculate available numbers smaller than x: (x - 1) - tree.query(x)

By summing up the contributions from each position, we get the total number of permutations that come before the given one, which is exactly the index we're looking for.

Learn more about Segment Tree, Binary Search, Divide and Conquer and Merge Sort patterns.

Solution Approach

The solution implements the counting strategy using a Binary Indexed Tree (Fenwick Tree) for efficient tracking of used numbers.

Binary Indexed Tree Setup: The BinaryIndexedTree class maintains an array c where we track which numbers have been used. The tree supports:

  • update(x, delta): Marks position x with a value change of delta
  • query(x): Returns the sum of all values from position 1 to x

Main Algorithm:

  1. Initialize factorial array: Create an array f where f[i] = i! to store factorials from 0! to (n-1)!. This avoids recalculating factorials repeatedly.

    f = [1] * n
    for i in range(1, n):
        f[i] = f[i - 1] * i % mod
  2. Process each position: For each element perm[i] at position i:

    • Calculate how many unused numbers are smaller than perm[i]:

      cnt = x - 1 - tree.query(x)

      Here, x - 1 is the total count of numbers smaller than x, and tree.query(x) tells us how many of those have already been used.

    • Add the contribution to the answer:

      ans += cnt * f[n - i - 1] % mod

      This multiplies the count of available smaller numbers by (n-i-1)!, which is the number of ways to arrange the remaining positions.

    • Mark the current number as used:

      tree.update(x, 1)

Example Walkthrough: For perm = [3, 1, 4, 2]:

  • Position 0, value 3: cnt = 3 - 1 - 0 = 2 smaller unused numbers (1 and 2). Add 2 × 3! = 12
  • Position 1, value 1: cnt = 1 - 1 - 0 = 0 smaller unused numbers. Add 0 × 2! = 0
  • Position 2, value 4: cnt = 4 - 1 - 2 = 1 smaller unused number (2). Add 1 × 1! = 1
  • Position 3, value 2: cnt = 2 - 1 - 1 = 0 smaller unused numbers. Add 0 × 0! = 0
  • Total: 12 + 0 + 1 + 0 = 13

The time complexity is O(n log n) due to the Binary Indexed Tree operations, and the space complexity is O(n) for storing the tree and factorial array.

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 finding the index of permutation [2, 3, 1] where n = 3.

Step 1: Initialize

  • Create factorial array: f = [1, 1, 2] (representing 0!, 1!, 2!)
  • Create Binary Indexed Tree to track used numbers
  • Set ans = 0

Step 2: Process position 0 (value = 2)

  • Numbers smaller than 2: {1}
  • Count unused numbers smaller than 2:
    • Total smaller: 2 - 1 = 1
    • Already used: tree.query(2) = 0
    • Unused smaller: 1 - 0 = 1
  • Contribution: 1 × f[3-0-1] = 1 × 2! = 2
  • Update ans = 0 + 2 = 2
  • Mark 2 as used: tree.update(2, 1)

Step 3: Process position 1 (value = 3)

  • Numbers smaller than 3: {1, 2}
  • Count unused numbers smaller than 3:
    • Total smaller: 3 - 1 = 2
    • Already used: tree.query(3) = 1 (only 2 is used)
    • Unused smaller: 2 - 1 = 1
  • Contribution: 1 × f[3-1-1] = 1 × 1! = 1
  • Update ans = 2 + 1 = 3
  • Mark 3 as used: tree.update(3, 1)

Step 4: Process position 2 (value = 1)

  • Numbers smaller than 1: none
  • Count unused numbers smaller than 1:
    • Total smaller: 1 - 1 = 0
    • Already used: tree.query(1) = 0
    • Unused smaller: 0 - 0 = 0
  • Contribution: 0 × f[3-2-1] = 0 × 0! = 0
  • Update ans = 3 + 0 = 3
  • Mark 1 as used: tree.update(1, 1)

Final Result: 3

This means [2, 3, 1] is at index 3 in the lexicographically sorted list of all permutations of [1, 2, 3].

We can verify this by listing all permutations:

  • Index 0: [1, 2, 3]
  • Index 1: [1, 3, 2]
  • Index 2: [2, 1, 3]
  • Index 3: [2, 3, 1] ✓
  • Index 4: [3, 1, 2]
  • Index 5: [3, 2, 1]

Solution Implementation

1from typing import List
2
3
4class BinaryIndexedTree:
5    """
6    Binary Indexed Tree (Fenwick Tree) for efficient prefix sum queries and updates.
7    Used to track which numbers have been used in the permutation.
8    """
9    __slots__ = "size", "tree"
10  
11    def __init__(self, n: int):
12        """
13        Initialize a Binary Indexed Tree with size n.
14      
15        Args:
16            n: The size of the tree (maximum value that can be stored)
17        """
18        self.size = n
19        self.tree = [0] * (n + 1)  # 1-indexed array for easier bit manipulation
20  
21    def update(self, x: int, delta: int) -> None:
22        """
23        Add delta to the value at position x and update all affected nodes.
24      
25        Args:
26            x: The position to update (1-indexed)
27            delta: The value to add at position x
28        """
29        while x <= self.size:
30            self.tree[x] += delta
31            # Move to the next node that this position affects
32            # x & -x gives the lowest set bit (LSB)
33            x += x & -x
34  
35    def query(self, x: int) -> int:
36        """
37        Calculate the prefix sum from index 1 to x (inclusive).
38      
39        Args:
40            x: The end position for the prefix sum (1-indexed)
41          
42        Returns:
43            The sum of all values from position 1 to x
44        """
45        prefix_sum = 0
46        while x > 0:
47            prefix_sum += self.tree[x]
48            # Move to the parent node by removing the lowest set bit
49            x -= x & -x
50        return prefix_sum
51
52
53class Solution:
54    def getPermutationIndex(self, perm: List[int]) -> int:
55        """
56        Calculate the lexicographic index of a given permutation.
57        The index represents how many permutations come before this one
58        in lexicographic order.
59      
60        Args:
61            perm: A list representing a permutation of integers from 1 to n
62          
63        Returns:
64            The 0-based lexicographic index of the permutation modulo 10^9 + 7
65        """
66        MOD = 10**9 + 7
67        result = 0
68        n = len(perm)
69      
70        # Initialize Binary Indexed Tree to track used numbers
71        fenwick_tree = BinaryIndexedTree(n + 1)
72      
73        # Precompute factorials for efficiency
74        # factorial[i] = i! mod MOD
75        factorial = [1] * n
76        for i in range(1, n):
77            factorial[i] = factorial[i - 1] * i % MOD
78      
79        # Process each position in the permutation
80        for position, value in enumerate(perm):
81            # Count how many smaller unused numbers could have been placed here
82            # This is: (numbers less than value) - (numbers less than value already used)
83            used_smaller_count = fenwick_tree.query(value)
84            available_smaller_count = value - 1 - used_smaller_count
85          
86            # Add the number of permutations that would come before this one
87            # if we had chosen one of the smaller available numbers
88            remaining_positions = n - position - 1
89            permutations_before = available_smaller_count * factorial[remaining_positions] % MOD
90            result = (result + permutations_before) % MOD
91          
92            # Mark this number as used in the tree
93            fenwick_tree.update(value, 1)
94      
95        return result
96
1/**
2 * Binary Indexed Tree (Fenwick Tree) for efficient range sum queries and point updates
3 */
4class BinaryIndexedTree {
5    private int size;
6    private int[] tree;
7
8    /**
9     * Initialize Binary Indexed Tree with given size
10     * @param size the maximum number of elements (1-indexed)
11     */
12    public BinaryIndexedTree(int size) {
13        this.size = size;
14        this.tree = new int[size + 1];  // 1-indexed array
15    }
16
17    /**
18     * Update the value at index by adding delta
19     * @param index the position to update (1-indexed)
20     * @param delta the value to add
21     */
22    public void update(int index, int delta) {
23        // Traverse all affected nodes in the tree
24        while (index <= size) {
25            tree[index] += delta;
26            // Move to next node: add the least significant bit
27            index += index & (-index);
28        }
29    }
30
31    /**
32     * Query the prefix sum from 1 to index
33     * @param index the end position of the range (1-indexed)
34     * @return the sum of elements from 1 to index
35     */
36    public int query(int index) {
37        int sum = 0;
38        // Traverse ancestors to calculate prefix sum
39        while (index > 0) {
40            sum += tree[index];
41            // Move to parent node: remove the least significant bit
42            index -= index & (-index);
43        }
44        return sum;
45    }
46}
47
48/**
49 * Solution class to find the lexicographic index of a permutation
50 */
51class Solution {
52    /**
53     * Calculate the lexicographic index of the given permutation
54     * The index represents the position of this permutation among all possible
55     * permutations when sorted lexicographically (0-indexed)
56     * 
57     * @param perm an array representing a permutation of numbers 1 to n
58     * @return the lexicographic index of the permutation modulo 10^9 + 7
59     */
60    public int getPermutationIndex(int[] perm) {
61        final int MOD = 1000000007;
62        long result = 0;
63        int n = perm.length;
64      
65        // Binary Indexed Tree to track which numbers have been used
66        BinaryIndexedTree fenwickTree = new BinaryIndexedTree(n + 1);
67      
68        // Precompute factorials: factorial[i] = i!
69        long[] factorial = new long[n];
70        factorial[0] = 1;
71        for (int i = 1; i < n; i++) {
72            factorial[i] = (factorial[i - 1] * i) % MOD;
73        }
74      
75        // Process each position in the permutation
76        for (int i = 0; i < n; i++) {
77            // Count how many unused numbers are smaller than current number
78            // perm[i] - 1: total numbers smaller than perm[i]
79            // fenwickTree.query(perm[i]): how many smaller numbers already used
80            int smallerUnusedCount = perm[i] - 1 - fenwickTree.query(perm[i]);
81          
82            // Add contribution to the index:
83            // smallerUnusedCount * (n-i-1)! represents permutations starting
84            // with smaller unused numbers at position i
85            result = (result + (smallerUnusedCount * factorial[n - i - 1]) % MOD) % MOD;
86          
87            // Mark current number as used
88            fenwickTree.update(perm[i], 1);
89        }
90      
91        return (int) result;
92    }
93}
94
1class BinaryIndexedTree {
2private:
3    int size;                    // Size of the tree (1-indexed)
4    vector<int> tree;            // Tree array to store cumulative frequencies
5
6public:
7    // Constructor: Initialize BIT with given size
8    BinaryIndexedTree(int n) : size(n), tree(n + 1) {}
9
10    // Update: Add delta to position x and propagate to affected nodes
11    void update(int x, int delta) {
12        // Traverse upward through the tree using lowbit operation
13        for (; x <= size; x += x & -x) {
14            tree[x] += delta;
15        }
16    }
17
18    // Query: Get prefix sum from index 1 to x
19    int query(int x) {
20        int sum = 0;
21        // Traverse downward through the tree using lowbit operation
22        for (; x > 0; x -= x & -x) {
23            sum += tree[x];
24        }
25        return sum;
26    }
27};
28
29class Solution {
30public:
31    int getPermutationIndex(vector<int>& perm) {
32        const int MOD = 1e9 + 7;
33        using ll = long long;
34      
35        int n = perm.size();
36        ll result = 0;
37      
38        // Initialize Binary Indexed Tree for tracking used elements
39        BinaryIndexedTree fenwickTree(n + 1);
40      
41        // Precompute factorials: factorial[i] = i!
42        ll factorial[n];
43        factorial[0] = 1;
44        for (int i = 1; i < n; ++i) {
45            factorial[i] = factorial[i - 1] * i % MOD;
46        }
47      
48        // Process each position in the permutation
49        for (int i = 0; i < n; ++i) {
50            // Count elements smaller than perm[i] that haven't been used yet
51            // perm[i] - 1: total elements smaller than perm[i]
52            // fenwickTree.query(perm[i]): elements smaller than perm[i] already used
53            int smallerUnusedCount = perm[i] - 1 - fenwickTree.query(perm[i]);
54          
55            // Add contribution to result: 
56            // smallerUnusedCount * (n-i-1)! permutations with smaller element at position i
57            result = (result + (ll)smallerUnusedCount * factorial[n - i - 1] % MOD) % MOD;
58          
59            // Mark current element as used in the fenwick tree
60            fenwickTree.update(perm[i], 1);
61        }
62      
63        return result % MOD;
64    }
65};
66
1// Binary Indexed Tree (Fenwick Tree) for efficient prefix sum queries and updates
2let treeSize: number;
3let treeArray: number[];
4
5/**
6 * Initializes the Binary Indexed Tree with given size
7 * @param n - The size of the tree
8 */
9function initializeTree(n: number): void {
10    treeSize = n;
11    treeArray = Array(n + 1).fill(0);
12}
13
14/**
15 * Updates the tree by adding delta to position x
16 * Uses the property that x & -x gives the rightmost set bit
17 * @param x - The position to update (1-indexed)
18 * @param delta - The value to add at position x
19 */
20function update(x: number, delta: number): void {
21    // Traverse all affected nodes in the tree
22    for (; x <= treeSize; x += x & -x) {
23        treeArray[x] += delta;
24    }
25}
26
27/**
28 * Queries the prefix sum from index 1 to x
29 * @param x - The ending position for the prefix sum (1-indexed)
30 * @returns The sum of elements from index 1 to x
31 */
32function query(x: number): number {
33    let sum = 0;
34    // Traverse the tree backwards to accumulate the sum
35    for (; x > 0; x -= x & -x) {
36        sum += treeArray[x];
37    }
38    return sum;
39}
40
41/**
42 * Calculates the lexicographic index of a permutation
43 * The index represents how many permutations come before this one in lexicographic order
44 * @param perm - An array representing a permutation of numbers from 1 to n
45 * @returns The lexicographic index of the permutation modulo 10^9 + 7
46 */
47function getPermutationIndex(perm: number[]): number {
48    const MOD = 1e9 + 7;
49    const permutationLength = perm.length;
50  
51    // Initialize the Binary Indexed Tree
52    initializeTree(permutationLength + 1);
53  
54    let result = 0;
55  
56    // Precompute factorials: factorial[i] = i!
57    const factorial: number[] = Array(permutationLength).fill(1);
58    for (let i = 1; i < permutationLength; ++i) {
59        factorial[i] = (factorial[i - 1] * i) % MOD;
60    }
61  
62    // Process each element in the permutation
63    for (let i = 0; i < permutationLength; ++i) {
64        // Count how many unused numbers are smaller than current element
65        // This is: (current element - 1) - (count of used numbers smaller than current)
66        const smallerUnusedCount = perm[i] - 1 - query(perm[i]);
67      
68        // Add the contribution of this position to the result
69        // smallerUnusedCount * (n-i-1)! represents all permutations with smaller element at position i
70        result = (result + smallerUnusedCount * factorial[permutationLength - i - 1]) % MOD;
71      
72        // Mark the current element as used in the tree
73        update(perm[i], 1);
74    }
75  
76    return result % MOD;
77}
78

Time and Space Complexity

Time Complexity: O(n × log n)

The time complexity is determined by analyzing the main operations:

  • The outer loop in getPermutationIndex runs n times, where n is the length of the permutation
  • Inside each iteration:
    • tree.query(x) is called once, which has O(log n) complexity due to the while loop that runs at most log n times (it removes the least significant bit in each iteration with x -= x & -x)
    • tree.update(x, 1) is called once, which also has O(log n) complexity due to the while loop that runs at most log n times (it adds the least significant bit in each iteration with x += x & -x)
    • Other operations like modular arithmetic are O(1)
  • Therefore, the total time complexity is O(n) × O(log n) = O(n × log n)

Space Complexity: O(n)

The space complexity comes from:

  • The BinaryIndexedTree's array self.c of size n + 1: O(n)
  • The factorial array f of size n: O(n)
  • Other variables use constant space: O(1)
  • Total space complexity is O(n) + O(n) + O(1) = O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Counting of Available Numbers

Pitfall: A common mistake is incorrectly calculating how many smaller numbers are still available at each position. Developers often forget to subtract the already-used numbers or miscalculate the formula.

Incorrect approach:

# Wrong: Just counting all smaller numbers
available_smaller_count = value - 1

Correct approach:

# Right: Count smaller numbers minus those already used
used_smaller_count = fenwick_tree.query(value)
available_smaller_count = value - 1 - used_smaller_count

2. Off-by-One Errors with Tree Indexing

Pitfall: Binary Indexed Trees are typically 1-indexed, but the permutation might be 0-indexed. Mixing these indexing systems leads to incorrect results.

Incorrect approach:

# Wrong: Using 0-based indexing with BIT
tree = BinaryIndexedTree(n)  # Should be n+1 for values 1 to n
tree.update(perm[i] - 1, 1)  # Wrong adjustment

Correct approach:

# Right: BIT sized for values 1 to n
tree = BinaryIndexedTree(n + 1)
tree.update(value, 1)  # Direct use since perm contains values 1 to n

3. Integer Overflow with Factorials

Pitfall: Factorials grow extremely fast. Without proper modulo operations, intermediate calculations will overflow even with 64-bit integers.

Incorrect approach:

# Wrong: No modulo during factorial computation
factorial = [1] * n
for i in range(1, n):
    factorial[i] = factorial[i - 1] * i  # Can overflow!

# Wrong: Late modulo application
result += available_smaller_count * factorial[remaining_positions]
result %= MOD  # Too late, overflow already happened

Correct approach:

# Right: Apply modulo at each step
factorial = [1] * n
for i in range(1, n):
    factorial[i] = factorial[i - 1] * i % MOD

# Right: Modulo during multiplication
permutations_before = available_smaller_count * factorial[remaining_positions] % MOD
result = (result + permutations_before) % MOD

4. Forgetting to Mark Numbers as Used

Pitfall: After processing each position, forgetting to update the Binary Indexed Tree means the same number could be incorrectly counted as available in subsequent positions.

Incorrect approach:

for position, value in enumerate(perm):
    available_smaller_count = value - 1 - fenwick_tree.query(value)
    result += available_smaller_count * factorial[n - position - 1] % MOD
    # Forgot to mark as used!

Correct approach:

for position, value in enumerate(perm):
    available_smaller_count = value - 1 - fenwick_tree.query(value)
    result = (result + available_smaller_count * factorial[n - position - 1]) % MOD
    fenwick_tree.update(value, 1)  # Mark this number as used

5. Misunderstanding the Problem Requirements

Pitfall: Confusing whether the result should be 0-indexed or 1-indexed. The problem asks for the 0-based index, meaning the first permutation has index 0.

Incorrect approach:

# Wrong: Adding 1 for 1-based indexing
return (result + 1) % MOD

Correct approach:

# Right: Return 0-based index directly
return result
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

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

Load More