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:
-
Find the largest index
i
such that1 <= i < s.length
ands[i] < s[i - 1]
. This means finding the rightmost position where a character is smaller than the character before it. -
Find the largest index
j
such thati <= j < s.length
ands[k] < s[i - 1]
for all possible values ofk
in the range[i, j]
inclusive. This means finding the rightmost position starting fromi
where all characters from positioni
toj
are smaller thans[i - 1]
. -
Swap the two characters at indices
i - 1
andj
. -
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ₖ!)
wheren
is the total number of remaining characters andn₁, n₂, ..., nₖ
are the frequencies of each distinct character
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
wheref[i] = i!
(factorial of i) - Array
g
whereg[i]
is the modular inverse off[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
-
Initialize a frequency counter for all characters in the string using
Counter(s)
. -
Traverse the string from left to right. For each position
i
with characterc
:a. Count smaller characters: Find how many characters in the remaining frequency map are smaller than
c
. This gives usm
- 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 frequencyv
- 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 positioni
. Remove it from the counter if frequency becomes 0. - Start with
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 EvaluatorExample 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":
- "abc" (smallest)
- "acb"
- "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)
takesO(k)
time wherek
is the number of distinct characters - The inner loop
for v in cnt.values()
also iterates through at mostk
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
andg
each of sizen + 1
, contributingO(n)
space - Counter
cnt
stores at mostk
distinct characters and their counts, which isO(k)
and bounded byO(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
Which type of traversal does breadth first search do?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!