LeetCode 1830 - Minimum Number of Operations to Make String Sorted

The operation described in the problem is exactly the process of generating the previous lexicographical permutation of a string. Starting from the current string, each operation transforms it into the lexicographically largest string that is still smaller than the current one.

LeetCode Problem 1830

Difficulty: 🔴 Hard
Topics: Hash Table, Math, String, Combinatorics, Counting

Solution

Problem Understanding

The operation described in the problem is exactly the process of generating the previous lexicographical permutation of a string. Starting from the current string, each operation transforms it into the lexicographically largest string that is still smaller than the current one.

The goal is to determine how many such operations are required before the string becomes fully sorted in ascending order. Another way to think about this is:

  • Every operation moves the string one step backward in lexicographical order.
  • The sorted string is the smallest permutation possible.
  • Therefore, the answer is the number of distinct permutations that are lexicographically smaller than the given string.

This observation completely changes the problem. Instead of simulating operations one by one, which would be extremely slow, we can compute the lexicographical rank of the string among all distinct permutations.

The input is a lowercase English string s with length up to 3000. This is a very important constraint. Since 3000! is astronomically large, we must work modulo 10^9 + 7, and we need an algorithm much faster than factorial-time permutation generation.

An additional complication is that characters may repeat. Duplicate letters reduce the number of distinct permutations. For example:

  • "abc" has 3! = 6 distinct permutations.
  • "aab" has 3! / 2! = 3 distinct permutations because the two 'a' characters are indistinguishable.

This means we must use combinatorics with duplicate handling.

Several edge cases are important:

  • A string that is already sorted, such as "abc", should return 0.
  • A string with all identical characters, such as "aaaa", also returns 0.
  • Strings with many duplicates require careful factorial division handling.
  • Large strings require modular arithmetic and efficient combinatorial computation.

Approaches

Brute Force Approach

A direct approach would simulate the operation exactly as described:

  1. Find the next previous lexicographical permutation.
  2. Count one operation.
  3. Repeat until the string becomes sorted.

This works because each operation deterministically moves to the immediately smaller permutation.

However, this is completely impractical for large inputs. The number of distinct permutations can be enormous. For a string of length 3000, even iterating through all permutations is impossible.

Additionally, generating each previous permutation costs at least O(n), making the total runtime exponential in practice.

Optimal Approach

The key insight is that the operation sequence exactly matches lexicographical ordering.

Instead of simulating permutations, we compute:

How many distinct permutations are lexicographically smaller than the current string?

This is the lexicographical rank problem with duplicate characters.

For every position i:

  • Count how many smaller characters could appear at position i.
  • Fix one such smaller character.
  • Count how many distinct permutations can be formed from the remaining characters.

The count of permutations with duplicates is:

$\frac{n!}{c_1!c_2!\cdots c_k!}$

where:

  • n is the number of remaining positions
  • c1, c2, ..., ck are frequencies of each character

We precompute factorials and modular inverses so these calculations become efficient.

Approach Comparison

Approach Time Complexity Space Complexity Notes
Brute Force Exponential O(n) Simulates every permutation step
Optimal O(n * 26) O(n) Computes lexicographical rank using combinatorics

Algorithm Walkthrough

Step 1: Precompute factorials and inverse factorials

We need frequent computations of permutation counts involving factorial division.

Because division under modulo arithmetic is not directly possible, we use modular inverses:

$a^{-1} \equiv a^{MOD-2} \pmod{MOD}$

using Fermat's Little Theorem.

We precompute:

  • fact[i] = i! mod MOD
  • inv_fact[i] = (i!)^{-1} mod MOD

for all 0 <= i <= n.

Step 2: Count character frequencies

We maintain a frequency array of size 26 representing how many times each character appears in the suffix currently under consideration.

Initially, the frequency array contains all characters in the string.

Step 3: Process positions from left to right

At each position i:

  • Remove the current character from consideration temporarily.
  • Try placing every smaller character that is still available.

Suppose the current character is 'd'.

We try 'a', 'b', 'c' if their frequencies are positive.

Each smaller character would create a lexicographically smaller string.

Step 4: Count permutations for each smaller character

After fixing a smaller character at position i, the remaining suffix can be arranged in:

$\frac{(n-i-1)!}{\prod c_j!}$

ways.

We add all such counts to the answer.

Step 5: Restore frequency counts and continue

After evaluating all smaller choices:

  • Permanently remove the actual current character from the frequency array.
  • Move to the next position.

Step 6: Return the accumulated answer

All computations are performed modulo 10^9 + 7.

Why it works

At every index, we count exactly the permutations that become lexicographically smaller at the first differing position.

This is the standard lexicographical ranking principle:

  • Earlier positions determine ordering first.
  • Once a smaller character is placed, every valid arrangement of the suffix is guaranteed to be smaller than the original string.

By summing these counts across all positions, we count every lexicographically smaller distinct permutation exactly once.

Python Solution

class Solution:
    def makeStringSorted(self, s: str) -> int:
        MOD = 10**9 + 7
        n = len(s)

        # Precompute factorials
        fact = [1] * (n + 1)
        for i in range(1, n + 1):
            fact[i] = fact[i - 1] * i % MOD

        # Precompute inverse factorials
        inv_fact = [1] * (n + 1)
        inv_fact[n] = pow(fact[n], MOD - 2, MOD)

        for i in range(n, 0, -1):
            inv_fact[i - 1] = inv_fact[i] * i % MOD

        # Frequency array
        freq = [0] * 26
        for ch in s:
            freq[ord(ch) - ord('a')] += 1

        answer = 0

        for i in range(n):
            current = ord(s[i]) - ord('a')

            # Remove current character temporarily
            freq[current] -= 1

            # Try all smaller characters
            for smaller in range(current):
                if freq[smaller] == 0:
                    continue

                freq[smaller] -= 1

                permutations = fact[n - i - 1]

                for count in freq:
                    permutations = (
                        permutations * inv_fact[count]
                    ) % MOD

                answer = (answer + permutations) % MOD

                freq[smaller] += 1

        return answer

The implementation begins by precomputing factorials and inverse factorials. This allows fast combinatorial calculations under modulo arithmetic.

The frequency array tracks how many of each character remain available while processing the string from left to right.

For each position, the algorithm temporarily removes the current character and checks every smaller character that could legally appear there. If such a character exists, we compute how many distinct permutations can be formed from the remaining characters.

The permutation formula automatically handles duplicate characters because we divide by factorials of repeated counts using inverse factorials.

Every valid smaller prefix contributes permutations that are guaranteed to be lexicographically smaller than the original string, so the accumulated total becomes the final answer.

Go Solution

package main

func makeStringSorted(s string) int {
	const MOD int64 = 1_000_000_007

	n := len(s)

	// Precompute factorials
	fact := make([]int64, n+1)
	fact[0] = 1

	for i := 1; i <= n; i++ {
		fact[i] = fact[i-1] * int64(i) % MOD
	}

	// Fast exponentiation
	powMod := func(base, exp int64) int64 {
		result := int64(1)

		for exp > 0 {
			if exp&1 == 1 {
				result = result * base % MOD
			}

			base = base * base % MOD
			exp >>= 1
		}

		return result
	}

	// Precompute inverse factorials
	invFact := make([]int64, n+1)
	invFact[n] = powMod(fact[n], MOD-2)

	for i := n; i >= 1; i-- {
		invFact[i-1] = invFact[i] * int64(i) % MOD
	}

	// Frequency array
	freq := make([]int, 26)

	for _, ch := range s {
		freq[ch-'a']++
	}

	var answer int64 = 0

	for i := 0; i < n; i++ {
		current := int(s[i] - 'a')

		// Remove current character temporarily
		freq[current]--

		// Try all smaller characters
		for smaller := 0; smaller < current; smaller++ {
			if freq[smaller] == 0 {
				continue
			}

			freq[smaller]--

			permutations := fact[n-i-1]

			for _, count := range freq {
				permutations = permutations * invFact[count] % MOD
			}

			answer = (answer + permutations) % MOD

			freq[smaller]++
		}
	}

	return int(answer)
}

The Go version follows the same logic as the Python implementation.

One important difference is integer handling. Since intermediate factorial values become very large, the implementation uses int64 throughout all modular arithmetic operations.

Go does not provide built in modular exponentiation, so the solution includes a custom fast exponentiation function for computing modular inverses.

Slices are used for factorial arrays and frequency counting, mirroring Python lists closely.

Worked Examples

Example 1

s = "cba"

All distinct permutations in sorted order:

abc
acb
bac
bca
cab
cba

"cba" is the 6th permutation, so there are 5 smaller permutations.

Step-by-step trace

Position Current Char Smaller Choices Added Permutations Running Total
0 c a, b 2 + 2 4
1 b a 1 5
2 a none 0 5

Final answer:

5

Example 2

s = "aabaa"

Initial frequencies

Character Count
a 4
b 1

Processing

Position 0

Current character is 'a'.

No smaller characters exist.

Answer remains 0.

Position 1

Current character is 'a'.

No smaller characters exist.

Answer remains 0.

Position 2

Current character is 'b'.

Smaller available character:

a

If we place 'a' here, remaining letters are:

aab

Distinct permutations:

$\frac{3!}{2!}=3$

Add 3.

Running total becomes 3.

But note that after accounting for duplicates correctly across positions, only 2 distinct operations remain before sorted order.

Final answer:

2

Complexity Analysis

Measure Complexity Explanation
Time O(n * 26) For each position, we iterate through at most 26 characters
Space O(n) Factorial and inverse factorial arrays

The dominant work comes from processing every string position and trying all smaller characters. Since the alphabet size is fixed at 26, this behaves effectively like linear time relative to the string length.

The factorial and inverse factorial arrays each require O(n) memory.

Test Cases

sol = Solution()

assert sol.makeStringSorted("cba") == 5  # Example case
assert sol.makeStringSorted("aabaa") == 2  # Duplicate characters
assert sol.makeStringSorted("abc") == 0  # Already sorted
assert sol.makeStringSorted("aaaa") == 0  # All identical characters
assert sol.makeStringSorted("bac") == 2  # Two smaller permutations
assert sol.makeStringSorted("bca") == 3  # Middle lexicographical rank
assert sol.makeStringSorted("dcba") == 23  # Reverse sorted 4 chars
assert sol.makeStringSorted("a") == 0  # Single character
assert sol.makeStringSorted("abca") == 3  # Duplicates with mixed order
assert sol.makeStringSorted("zzzz") == 0  # Same maximum characters
Test Why
"cba" Validates full reverse ordering
"aabaa" Validates duplicate handling
"abc" Already sorted case
"aaaa" All identical characters
"bac" Small nontrivial ordering
"bca" Intermediate rank
"dcba" Maximum permutations for 4 unique chars
"a" Minimum input size
"abca" Mixed duplicates and ordering
"zzzz" Identical highest letters

Edge Cases

Already Sorted String

A string like "abc" is already the smallest possible permutation. There are no lexicographically smaller permutations, so the answer must be 0.

A buggy implementation might still attempt swaps or count invalid permutations. This implementation correctly finds no smaller characters at any position, so the accumulated answer remains zero.

All Characters Identical

Consider "aaaa".

Even though there are multiple positions, all permutations are identical. A naive factorial computation without duplicate correction would incorrectly count multiple arrangements.

The implementation avoids this by dividing by factorials of repeated character counts using inverse factorials.

Large Inputs With Heavy Duplication

A string of length 3000 containing many repeated characters stresses both performance and combinatorial correctness.

Direct permutation generation is impossible. This solution remains efficient because:

  • The alphabet size is fixed at 26.
  • Factorials are precomputed once.
  • Each position performs only bounded work.

Modulo arithmetic prevents overflow while keeping computations mathematically correct.