Facebook Pixel

1830. Minimum Number of Operations to Make String Sorted

Problem Description

You are given a string s (0-indexed). You need to perform a specific set of operations on the string repeatedly until it becomes sorted. Each operation consists of four steps:

  1. Find the largest index i such that 1 <= i < s.length and s[i] < s[i - 1]. This means finding the rightmost position where a character is smaller than the character before it.

  2. Find the largest index j such that i <= j < s.length and s[k] < s[i - 1] for all possible values of k in the range [i, j] inclusive. This means finding the rightmost position starting from i where all characters from position i to j are smaller than s[i - 1].

  3. Swap the two characters at indices i - 1 and j.

  4. Reverse the suffix starting at index i.

The problem asks you to return the number of operations needed to make the string sorted in ascending order. Since the answer can be very large, return it modulo 10^9 + 7.

The operation described is essentially finding the previous permutation of the current string in lexicographical order. The solution leverages the fact that counting the number of operations is equivalent to counting how many permutations of the string are lexicographically smaller than the given string. This is because each operation transforms the string into its immediate lexicographical predecessor, so the number of operations equals the number of permutations between the sorted string and the original string.

The solution uses combinatorics to calculate the number of smaller permutations by considering:

  • For each position, how many smaller characters could be placed there
  • The number of valid permutations for the remaining positions, accounting for duplicate characters using the formula: n! / (n₁! × n₂! × ... × nₖ!) where n is the total number of remaining characters and n₁, n₂, ..., nₖ are the frequencies of each distinct character
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that the described operation is actually finding the previous permutation in lexicographical order. Each operation transforms the string into the lexicographically previous permutation, moving step by step toward the sorted (smallest) permutation.

Think of it this way: if we start with "dcba" and keep applying the operation, we get "dcab" → "dbca" → "dbac" → ... → "abcd". Each step moves to the previous permutation until we reach the sorted string "abcd", which is the lexicographically smallest.

Since each operation moves exactly one step backward in the lexicographical ordering, the number of operations equals the number of permutations that come before our starting string in lexicographical order. This transforms the problem from simulation to counting.

To count permutations smaller than our string, we can think position by position from left to right. At each position i, we ask: "How many valid permutations would we get if we placed a smaller character here instead?"

For example, with string "dca":

  • At position 0, we could place 'a', 'b', or 'c' (all smaller than 'd'). Each choice would lead to permutations smaller than "dca"
  • For each such choice, we need to count how many ways we can arrange the remaining characters

The challenge with duplicate characters is handled using the multinomial coefficient formula. If we have n total characters with frequencies n₁, n₂, ..., nₖ for each distinct character, the number of unique permutations is n! / (n₁! × n₂! × ... × nₖ!).

This approach avoids simulating each operation (which would be too slow) and instead directly computes the answer using combinatorial mathematics. We precompute factorials and their modular inverses to efficiently calculate these values as we traverse the string.

Learn more about Math and Combinatorics patterns.

Solution Approach

The solution uses counting, permutation and combination, and preprocessing to efficiently calculate the answer.

Preprocessing Factorials and Modular Inverses

First, we precompute:

  • Array f where f[i] = i! (factorial of i)
  • Array g where g[i] is the modular inverse of f[i]

The modular inverse is calculated using Fermat's Little Theorem: for prime modulus p, the inverse of a is a^(p-2) mod p. This allows us to perform division under modulo by converting it to multiplication with the inverse.

n = 3010
mod = 10**9 + 7
f = [1] + [0] * n  # f[i] will store i!
g = [1] + [0] * n  # g[i] will store inverse of i!

for i in range(1, n):
    f[i] = f[i - 1] * i % mod
    g[i] = pow(f[i], mod - 2, mod)  # Fermat's Little Theorem

Main Algorithm

  1. Initialize a frequency counter for all characters in the string using Counter(s).

  2. Traverse the string from left to right. For each position i with character c:

    a. Count smaller characters: Find how many characters in the remaining frequency map are smaller than c. This gives us m - the number of ways we could place a smaller character at this position.

    b. Calculate permutations: For each way of placing a smaller character, we need to count the permutations of the remaining n - i - 1 characters. Using the multinomial coefficient:

    • Start with t = f[n - i - 1] * m (total remaining positions × number of smaller characters)
    • Divide by the factorial of each character's frequency: t = t * g[v] for each frequency v
    • This gives us m × (n-i-1)! / (n₁! × n₂! × ... × nₖ!)

    c. Add to answer: Add t to our running total (with modulo).

    d. Update frequency: Decrease the frequency of character c by 1, as we've "used" it at position i. Remove it from the counter if frequency becomes 0.

class Solution:
    def makeStringSorted(self, s: str) -> int:
        cnt = Counter(s)
        ans, n = 0, len(s)
      
        for i, c in enumerate(s):
            # Count characters smaller than c
            m = sum(v for a, v in cnt.items() if a < c)
          
            # Calculate permutations with smaller character at position i
            t = f[n - i - 1] * m
            for v in cnt.values():
                t = t * g[v] % mod
          
            # Add to answer
            ans = (ans + t) % mod
          
            # Update frequency counter
            cnt[c] -= 1
            if cnt[c] == 0:
                cnt.pop(c)
              
        return ans

The algorithm efficiently counts all lexicographically smaller permutations without generating them, achieving a time complexity of O(n × k) where n is the string length and k is the number of distinct characters (at most 26 for lowercase letters).

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 the string s = "bca".

Initial Setup:

  • String: "bca"
  • Frequency counter: {'b': 1, 'c': 1, 'a': 1}
  • We need to count how many permutations are lexicographically smaller than "bca"

Position 0 (character 'b'):

  • Current character: 'b'
  • Characters smaller than 'b' in our frequency map: only 'a' (count = 1)
  • So we have m = 1 way to place a smaller character here
  • If we place 'a' at position 0, we need to arrange the remaining 2 characters ('b' and 'c')
  • Number of permutations: 1 × 2! / (1! × 1!) = 1 × 2 / 1 = 2
  • These permutations are: "abc" and "acb" (both start with 'a' which is smaller than 'b')
  • Add 2 to our answer: ans = 2
  • Update frequency: {'b': 0, 'c': 1, 'a': 1} → {'c': 1, 'a': 1}

Position 1 (character 'c'):

  • Current character: 'c'
  • Characters smaller than 'c' in remaining frequency map: only 'a' (count = 1)
  • So we have m = 1 way to place a smaller character here
  • If we place 'a' at position 1 (keeping 'b' at position 0), we need to arrange the remaining 1 character ('c')
  • Number of permutations: 1 × 1! / 1! = 1
  • This permutation is: "bac" (which has 'ba' as prefix and is smaller than "bca")
  • Add 1 to our answer: ans = 2 + 1 = 3
  • Update frequency: {'c': 0, 'a': 1} → {'a': 1}

Position 2 (character 'a'):

  • Current character: 'a'
  • Characters smaller than 'a' in remaining frequency map: none
  • So m = 0
  • No permutations to add: ans = 3

Final Answer: 3

This means there are 3 permutations lexicographically smaller than "bca":

  1. "abc" (smallest)
  2. "acb"
  3. "bac"

And indeed, if we started with "bca" and applied the operation 3 times:

  • "bca" → "bac" (1st operation)
  • "bac" → "acb" (2nd operation)
  • "acb" → "abc" (3rd operation, now sorted)

The solution correctly counts these without simulating each operation!

Solution Implementation

1from collections import Counter
2from typing import List
3
4# Precompute factorials and their modular inverses
5MAX_N = 3010
6MOD = 10**9 + 7
7
8# factorial[i] = i! mod MOD
9factorial = [1] + [0] * MAX_N
10# factorial_inverse[i] = (i!)^(-1) mod MOD
11factorial_inverse = [1] + [0] * MAX_N
12
13for i in range(1, MAX_N):
14    factorial[i] = factorial[i - 1] * i % MOD
15    factorial_inverse[i] = pow(factorial[i], MOD - 2, MOD)
16
17
18class Solution:
19    def makeStringSorted(self, s: str) -> int:
20        """
21        Calculate the number of permutations of string s that come before s
22        in lexicographical order.
23      
24        The algorithm works by counting how many permutations would start with
25        a character smaller than the current character at each position.
26        """
27        # Count frequency of each character in the string
28        char_count = Counter(s)
29        result = 0
30        string_length = len(s)
31      
32        for position, current_char in enumerate(s):
33            # Count characters smaller than current character
34            smaller_chars_count = sum(
35                freq for char, freq in char_count.items() 
36                if char < current_char
37            )
38          
39            # Calculate number of permutations starting with a smaller character
40            # This is: (remaining_positions)! * smaller_chars_count / product(freq!)
41            remaining_positions = string_length - position - 1
42            permutations = factorial[remaining_positions] * smaller_chars_count
43          
44            # Divide by factorial of each character frequency (for duplicate handling)
45            for frequency in char_count.values():
46                permutations = permutations * factorial_inverse[frequency] % MOD
47          
48            # Add to result
49            result = (result + permutations) % MOD
50          
51            # Update character count for next iteration
52            char_count[current_char] -= 1
53            if char_count[current_char] == 0:
54                char_count.pop(current_char)
55      
56        return result
57
1class Solution {
2    private static final int MAX_LENGTH = 3010;
3    private static final int MODULO = (int) 1e9 + 7;
4    private static final long[] factorial = new long[MAX_LENGTH];
5    private static final long[] inverseFactorial = new long[MAX_LENGTH];
6
7    // Static block to precompute factorials and their modular inverses
8    static {
9        factorial[0] = 1;
10        inverseFactorial[0] = 1;
11      
12        // Compute factorial[i] = i! mod MODULO
13        for (int i = 1; i < MAX_LENGTH; ++i) {
14            factorial[i] = factorial[i - 1] * i % MODULO;
15            // Compute modular inverse of factorial[i] using Fermat's Little Theorem
16            inverseFactorial[i] = modularPower(factorial[i], MODULO - 2);
17        }
18    }
19
20    /**
21     * Computes (base^exponent) % MODULO using fast exponentiation
22     * @param base the base number
23     * @param exponent the power to raise the base to
24     * @return (base^exponent) % MODULO
25     */
26    public static long modularPower(long base, int exponent) {
27        long result = 1;
28      
29        while (exponent != 0) {
30            // If current bit is 1, multiply result with base
31            if ((exponent & 1) == 1) {
32                result = result * base % MODULO;
33            }
34            // Square the base for next bit
35            exponent >>= 1;
36            base = base * base % MODULO;
37        }
38      
39        return result;
40    }
41
42    /**
43     * Finds the lexicographic rank of the given string among all its permutations
44     * @param s the input string
45     * @return the number of permutations that come before s lexicographically
46     */
47    public int makeStringSorted(String s) {
48        // Count frequency of each character
49        int[] charFrequency = new int[26];
50        int stringLength = s.length();
51      
52        for (int i = 0; i < stringLength; ++i) {
53            ++charFrequency[s.charAt(i) - 'a'];
54        }
55      
56        long answer = 0;
57      
58        // Process each position in the string
59        for (int position = 0; position < stringLength; ++position) {
60            // Count characters smaller than current character
61            int smallerCharCount = 0;
62            for (int charIndex = s.charAt(position) - 'a' - 1; charIndex >= 0; --charIndex) {
63                smallerCharCount += charFrequency[charIndex];
64            }
65          
66            // Calculate permutations with smaller characters at current position
67            // Formula: (number of smaller chars) * (remaining positions)! / (product of frequency factorials)
68            long permutationsCount = smallerCharCount * factorial[stringLength - position - 1] % MODULO;
69          
70            // Divide by factorial of each character's frequency (handles duplicate characters)
71            for (int frequency : charFrequency) {
72                permutationsCount = permutationsCount * inverseFactorial[frequency] % MODULO;
73            }
74          
75            // Update frequency for current character as it's now fixed
76            --charFrequency[s.charAt(position) - 'a'];
77          
78            // Add to total count (adding MODULO ensures positive result)
79            answer = (answer + permutationsCount + MODULO) % MODULO;
80        }
81      
82        return (int) answer;
83    }
84}
85
1const int MAX_LENGTH = 3010;
2const int MOD = 1e9 + 7;
3long factorial[MAX_LENGTH];
4long inverseFactorial[MAX_LENGTH];
5
6/**
7 * Fast modular exponentiation to compute (base^exponent) % MOD
8 * Used to calculate modular multiplicative inverse
9 */
10long modularPower(long base, int exponent) {
11    long result = 1;
12    while (exponent != 0) {
13        if ((exponent & 1) == 1) {
14            result = result * base % MOD;
15        }
16        exponent >>= 1;
17        base = base * base % MOD;
18    }
19    return result;
20}
21
22// Pre-compute factorials and their modular inverses at compile time
23int precompute = []() {
24    factorial[0] = inverseFactorial[0] = 1;
25    for (int i = 1; i < MAX_LENGTH; ++i) {
26        // Calculate i! mod MOD
27        factorial[i] = factorial[i - 1] * i % MOD;
28        // Calculate modular inverse of i! using Fermat's Little Theorem
29        // Since MOD is prime: inverse(a) = a^(MOD-2) mod MOD
30        inverseFactorial[i] = modularPower(factorial[i], MOD - 2);
31    }
32    return 0;
33}();
34
35class Solution {
36public:
37    /**
38     * Find the 1-indexed position of string s among all its sorted permutations
39     * The position is calculated by counting permutations that come before s
40     */
41    int makeStringSorted(string s) {
42        // Count frequency of each character in the string
43        int charCount[26]{};
44        for (char& c : s) {
45            ++charCount[c - 'a'];
46        }
47      
48        int stringLength = s.size();
49        long rankPosition = 0;
50      
51        // Process each position from left to right
52        for (int position = 0; position < stringLength; ++position) {
53            // Count characters smaller than current character at this position
54            int smallerCharsCount = 0;
55            for (int charIndex = s[position] - 'a' - 1; charIndex >= 0; --charIndex) {
56                smallerCharsCount += charCount[charIndex];
57            }
58          
59            // Calculate number of permutations starting with a smaller character
60            // Formula: smallerCharsCount * (remaining positions)! / (product of factorials of char frequencies)
61            long permutationsWithSmallerStart = smallerCharsCount * factorial[stringLength - position - 1] % MOD;
62          
63            // Divide by factorial of each character's frequency to handle duplicates
64            for (int& frequency : charCount) {
65                permutationsWithSmallerStart = permutationsWithSmallerStart * inverseFactorial[frequency] % MOD;
66            }
67          
68            // Add to total rank (with MOD to handle potential negative values)
69            rankPosition = (rankPosition + permutationsWithSmallerStart + MOD) % MOD;
70          
71            // Remove current character from available characters
72            --charCount[s[position] - 'a'];
73        }
74      
75        return rankPosition;
76    }
77};
78
1const MAX_LENGTH = 3010;
2const MOD = 1000000007;
3const factorial: number[] = new Array(MAX_LENGTH);
4const inverseFactorial: number[] = new Array(MAX_LENGTH);
5
6/**
7 * Fast modular exponentiation to compute (base^exponent) % MOD
8 * Used to calculate modular multiplicative inverse
9 */
10function modularPower(base: number, exponent: number): number {
11    let result = 1;
12    base = base % MOD;
13  
14    while (exponent !== 0) {
15        // If exponent is odd, multiply base with result
16        if ((exponent & 1) === 1) {
17            result = (result * base) % MOD;
18        }
19        // Divide exponent by 2
20        exponent >>= 1;
21        // Square the base
22        base = (base * base) % MOD;
23    }
24    return result;
25}
26
27/**
28 * Pre-compute factorials and their modular inverses
29 * This initialization runs once when the module loads
30 */
31function precomputeFactorials(): void {
32    // Initialize base cases
33    factorial[0] = 1;
34    inverseFactorial[0] = 1;
35  
36    for (let i = 1; i < MAX_LENGTH; i++) {
37        // Calculate i! mod MOD
38        factorial[i] = (factorial[i - 1] * i) % MOD;
39        // Calculate modular inverse of i! using Fermat's Little Theorem
40        // Since MOD is prime: inverse(a) = a^(MOD-2) mod MOD
41        inverseFactorial[i] = modularPower(factorial[i], MOD - 2);
42    }
43}
44
45// Execute precomputation immediately
46precomputeFactorials();
47
48/**
49 * Find the 1-indexed position of string s among all its sorted permutations
50 * The position is calculated by counting permutations that come before s
51 */
52function makeStringSorted(s: string): number {
53    // Count frequency of each character in the string (26 lowercase letters)
54    const charCount: number[] = new Array(26).fill(0);
55    for (const char of s) {
56        charCount[char.charCodeAt(0) - 97]++; // 'a'.charCodeAt(0) = 97
57    }
58  
59    const stringLength = s.length;
60    let rankPosition = 0;
61  
62    // Process each position from left to right
63    for (let position = 0; position < stringLength; position++) {
64        const currentCharIndex = s.charCodeAt(position) - 97;
65      
66        // Count characters smaller than current character at this position
67        let smallerCharsCount = 0;
68        for (let charIndex = currentCharIndex - 1; charIndex >= 0; charIndex--) {
69            smallerCharsCount += charCount[charIndex];
70        }
71      
72        // Calculate number of permutations starting with a smaller character
73        // Formula: smallerCharsCount * (remaining positions)! / (product of factorials of char frequencies)
74        let permutationsWithSmallerStart = (smallerCharsCount * factorial[stringLength - position - 1]) % MOD;
75      
76        // Divide by factorial of each character's frequency to handle duplicates
77        for (const frequency of charCount) {
78            permutationsWithSmallerStart = (permutationsWithSmallerStart * inverseFactorial[frequency]) % MOD;
79        }
80      
81        // Add to total rank (ensure non-negative result with MOD)
82        rankPosition = (rankPosition + permutationsWithSmallerStart + MOD) % MOD;
83      
84        // Remove current character from available characters
85        charCount[currentCharIndex]--;
86    }
87  
88    return rankPosition;
89}
90

Time and Space Complexity

Time Complexity: O(n × k)

The main loop iterates through each character in string s (length n). For each iteration:

  • Computing m = sum(v for a, v in cnt.items() if a < c) takes O(k) time where k is the number of distinct characters
  • The inner loop for v in cnt.values() also iterates through at most k values
  • Other operations like modular arithmetic and dictionary updates are O(1)

Therefore, the overall time complexity is O(n × k).

Space Complexity: O(n)

The space usage includes:

  • Arrays f and g each of size n + 1, contributing O(n) space
  • Counter cnt stores at most k distinct characters and their counts, which is O(k) and bounded by O(n)
  • Other variables use constant space

Since k ≤ n (the number of distinct characters cannot exceed the string length), and the arrays f and g dominate the space usage, the overall space complexity is O(n).

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

Common Pitfalls

1. Integer Overflow Without Proper Modulo Operations

One of the most common pitfalls is forgetting to apply modulo operations at every multiplication step, which can lead to integer overflow even in languages like Python that handle big integers.

Incorrect approach:

# Wrong - might overflow before taking modulo
permutations = factorial[remaining_positions] * smaller_chars_count
for frequency in char_count.values():
    permutations = permutations * factorial_inverse[frequency]
result = (result + permutations) % MOD  # Too late!

Correct approach:

# Apply modulo after each multiplication
permutations = factorial[remaining_positions] * smaller_chars_count % MOD
for frequency in char_count.values():
    permutations = permutations * factorial_inverse[frequency] % MOD
result = (result + permutations) % MOD

2. Incorrect Precomputation Bounds

Another pitfall is setting the precomputation array size too small. If MAX_N is less than the actual string length, the code will crash with an index out of bounds error.

Problem scenario:

MAX_N = 100  # Too small!
# If string length is 3000, factorial[2999] will cause IndexError

Solution: Always set MAX_N to be at least as large as the maximum possible string length plus some buffer:

MAX_N = 3010  # Safe for strings up to length 3000

3. Not Handling Character Removal from Counter Properly

Forgetting to remove characters with zero frequency from the counter can lead to incorrect calculations in subsequent iterations.

Incorrect approach:

char_count[current_char] -= 1
# Forgot to remove when count becomes 0
# This leaves 0-frequency entries that affect the division calculation

Correct approach:

char_count[current_char] -= 1
if char_count[current_char] == 0:
    char_count.pop(current_char)  # Remove to avoid division by 0!

4. Misunderstanding the Multinomial Coefficient Formula

A subtle pitfall is incorrectly implementing the multinomial coefficient. The formula should divide by the factorial of ALL character frequencies, not just the ones being considered for placement.

Wrong interpretation:

# Incorrect - only dividing by frequency of smaller characters
for char, freq in char_count.items():
    if char < current_char:
        permutations = permutations * factorial_inverse[freq] % MOD

Correct implementation:

# Correct - divide by factorial of ALL remaining character frequencies
for frequency in char_count.values():
    permutations = permutations * factorial_inverse[frequency] % MOD

5. Off-by-One Error in Remaining Positions

Calculating the wrong number of remaining positions is a common mistake that leads to incorrect permutation counts.

Common mistake:

remaining_positions = string_length - position  # Wrong!
# This includes the current position

Correct calculation:

remaining_positions = string_length - position - 1  # Correct
# Excludes the current position since we're fixing it
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which type of traversal does breadth first search do?


Recommended Readings

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

Load More