Minimum Number of Operations to Make String Sorted
HardPrompt
You are given a string s (0-indexed). You are asked to perform the following operation on s until you get a sorted string:
- Find the largest index
isuch that1 <= i < s.lengthands[i] < s[i - 1]. - Find the largest index
jsuch thati <= j < s.lengthands[k] < s[i - 1]for all the possible values ofkin the range[i, j]inclusive. - Swap the two characters at indices
i - 1 andj. - Reverse the suffix starting at index
i.
Return the number of operations needed to make the string sorted. Since the answer can be too large, return it modulo 109 + 7.
Example 1:
Input: s = "cba"
Output: 5
Explanation: The simulation goes as follows:
Operation 1: i=2, j=2. Swap s[1] and s[2] to get s="cab", then reverse the suffix starting at 2. Now, s="cab".
Operation 2: i=1, j=2. Swap s[0] and s[2] to get s="bac", then reverse the suffix starting at 1. Now, s="bca".
Operation 3: i=2, j=2. Swap s[1] and s[2] to get s="bac", then reverse the suffix starting at 2. Now, s="bac".
Operation 4: i=1, j=1. Swap s[0] and s[1] to get s="abc", then reverse the suffix starting at 1. Now, s="acb".
Operation 5: i=2, j=2. Swap s[1] and s[2] to get s="abc", then reverse the suffix starting at 2. Now, s="abc".Example 2:
Input: s = "aabaa"
Output: 2
Explanation: The simulation goes as follows:
Operation 1: i=3, j=4. Swap s[2] and s[4] to get s="aaaab", then reverse the substring starting at 3. Now, s="aaaba".
Operation 2: i=4, j=4. Swap s[3] and s[4] to get s="aaaab", then reverse the substring starting at 4. Now, s="aaaab".
Constraints:
1 <= s.length <= 3000s consists only of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. We start with the given string and repeatedly apply the "previous permutation" operation until the string becomes sorted. We count the number of operations performed.
Algorithm
Initialize count = 0.Convert the input string s to a character array chars.Start an infinite loop:Find the largest index i such that 1 <= i < n and chars[i] < chars[i-1].If no such i exists, the string is sorted. Break the loop.Find the largest index j such that i <= j < n and chars[j] < chars[i-1].Swap chars[i-1] and chars[j].Reverse the subarray of chars from index i to the end.Increment count (modulo 10^9 + 7).
Return count.
Walkthrough
The algorithm involves a loop that continues as long as the string is not sorted. Inside the loop, we perform one operation as defined:
- Find the largest index
iwheres[i] < s[i-1]. This identifies the pivot point for generating the previous permutation. - Find the largest index
jin the suffixs[i:]such thats[j] < s[i-1]. This finds the character to swap with the pivot. - Swap the characters at
i-1andj. - Reverse the suffix of the string starting from index
i. We maintain a counter which is incremented in each iteration of the loop. The simulation stops when the string is lexicographically sorted, and we return the final count. Since the string is modified in place, we convert it to a character array for easier manipulation.
class Solution { public int makeStringSorted(String s) { long count = 0; long MOD = 1_000_000_007; char[] chars = s.toCharArray(); int n = s.length(); while (true) { int i = n - 1; while (i > 0 && chars[i] >= chars[i - 1]) { i--; } if (i == 0) { // String is sorted break; } int pivot = i - 1; int j = n - 1; while (j >= i && chars[j] >= chars[pivot]) { j--; } swap(chars, pivot, j); reverse(chars, i, n - 1); count = (count + 1) % MOD; } return (int) count; } private void swap(char[] chars, int i, int j) { char temp = chars[i]; chars[i] = chars[j]; chars[j] = temp; } private void reverse(char[] chars, int start, int end) { while (start < end) { swap(chars, start, end); start++; end--; } }}Complexity
Time
O(K * N), where `N` is the length of the string and `K` is the total number of operations. `K` can be very large, making this approach impractical.
Space
O(N) to store the character array.
Trade-offs
Pros
Simple to understand and implement as it directly follows the problem description.
Cons
Extremely inefficient. The number of operations can be astronomically large (related to
n!), causing the simulation to be too slow for the given constraints (nup to 3000). This will result in a Time Limit Exceeded (TLE) error.
This approach reframes the problem from simulation to a mathematical counting problem. The number of operations required to sort the string is equal to the number of unique permutations of its characters that are lexicographically smaller than the given string s. We can calculate this count by iterating through the string from left to right.
Algorithm
Precompute factorials and their modular inverses up to n.Calculate the frequency of each character in the input string s.Calculate an initial denominator term: product of invFact[freq[c]] for all c.Initialize total_operations = 0.Iterate i from 0 to n-1:Let m = n - i be the number of remaining characters.Count the number of available characters smaller than s[i] by iterating through the frequency array. Let this be count_smaller.Calculate the contribution for this step: count_smaller * (m-1)! * (current denominator inverse).Add this contribution to total_operations (modulo 10^9 + 7).Update the denominator inverse for the next iteration by multiplying by the current frequency of s[i].Decrement the frequency of character s[i].
Return total_operations.
Walkthrough
The core idea is to count, for each position i from 0 to n-1, how many smaller permutations we can form by choosing a different character for this position. We iterate through the string s from left to right. At each position i, we consider the suffix of the string starting at i. Let the set of available characters for the suffix be C with frequencies freq, and the length of the suffix be m = n - i. We count how many characters in C are smaller than s[i]. Let this be count_smaller. The number of permutations of the remaining m-1 characters is (m-1)! / (product of freq[c]!). The total contribution from position i is count_smaller * (m-1)! / (product of freq[c]!). We sum these contributions for all i. To handle large numbers and divisions, all calculations are done modulo 10^9 + 7, requiring precomputed factorials and their modular inverses.
class Solution { long[] fact; long[] invFact; long MOD = 1_000_000_007; public int makeStringSorted(String s) { int n = s.length(); precomputeFactorials(n); int[] freq = new int[26]; for (char c : s.toCharArray()) { freq[c - 'a']++; } long permsDenominatorInv = 1; for (int count : freq) { permsDenominatorInv = (permsDenominatorInv * invFact[count]) % MOD; } long totalOps = 0; for (int i = 0; i < n; i++) { int remainingChars = n - i; int charCode = s.charAt(i) - 'a'; long countSmaller = 0; for (int j = 0; j < charCode; j++) { countSmaller += freq[j]; } if (countSmaller > 0) { long term = (fact[remainingChars - 1] * countSmaller) % MOD; long contribution = (term * permsDenominatorInv) % MOD; totalOps = (totalOps + contribution) % MOD; } permsDenominatorInv = (permsDenominatorInv * freq[charCode]) % MOD; freq[charCode]--; } return (int) totalOps; } private void precomputeFactorials(int n) { fact = new long[n + 1]; invFact = new long[n + 1]; fact[0] = 1; invFact[0] = 1; for (int i = 1; i <= n; i++) { fact[i] = (fact[i - 1] * i) % MOD; invFact[i] = power(fact[i], MOD - 2); } } private long power(long base, long exp) { long res = 1; base %= MOD; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % MOD; base = (base * base) % MOD; exp /= 2; } return res; }}Complexity
Time
O(N * A), where `N` is the string length and `A` is the alphabet size (26). Precomputation takes O(N * log(MOD)).
Space
O(N + A) for storing factorials, their inverses, and frequency counts, where A is the alphabet size.
Trade-offs
Pros
Significantly more efficient than simulation.
Avoids the TLE issue by using a mathematical formula instead of step-by-step transformation.
Correctly handles strings with duplicate characters.
Cons
The calculation of the count of smaller characters is done in
O(A)inside the main loop, leading to anO(N * A)complexity. This can be further optimized.
This approach builds upon the combinatorial counting method by optimizing the two main operations inside the loop: counting smaller characters and calculating the permutation denominator. A Fenwick Tree (or Binary Indexed Tree) is used to speed up the query for smaller character counts, and the denominator term is updated incrementally.
Algorithm
Precompute factorials up to n.Calculate initial character frequencies and initialize a Fenwick Tree with them.Calculate an initial denominator term: product of invFact[freq[c]] for all c.Initialize total_operations = 0.Iterate i from 0 to n-1:Let m = n - i.Use the Fenwick Tree to query the count of available characters smaller than s[i] in O(log A) time.Calculate the contribution for this step using the precomputed factorials and the current denominator term.Add the contribution to total_operations.Update the denominator term for the next iteration in O(1) by multiplying by the current frequency of s[i].Update the Fenwick Tree by decrementing the count of s[i] in O(log A) time.
Return total_operations.
Walkthrough
The overall logic is the same as the previous combinatorial approach. The key difference lies in the implementation details for efficiency.
- Fenwick Tree for Smaller Count: We use a Fenwick Tree of size 26 to maintain the character frequencies. To find the number of available characters smaller than
s[i], we can query the prefix sum in the Fenwick Tree up tos[i] - 'a'. This operation takesO(log A)time, whereAis the alphabet size. After processings[i], we update the Fenwick Tree by decrementing the count fors[i], which also takesO(log A). - Incremental Denominator Update: Instead of recomputing the product of inverse factorials in each step, we update it incrementally. When we use up a character
c, its frequencyf_cbecomesf_c - 1. The denominator inverse term gets multiplied byf_c. This update takesO(1)time. By combining these two optimizations, the work inside each iteration of the main loop is reduced significantly.
class Solution { long[] fact; long MOD = 1_000_000_007; int ALPHABET_SIZE = 26; public int makeStringSorted(String s) { int n = s.length(); precomputeFactorials(n); int[] freq = new int[ALPHABET_SIZE]; for (char c : s.toCharArray()) { freq[c - 'a']++; } FenwickTree bit = new FenwickTree(ALPHABET_SIZE); for (int i = 0; i < ALPHABET_SIZE; i++) { bit.update(i + 1, freq[i]); } long permsDenominatorInv = 1; for (int count : freq) { permsDenominatorInv = (permsDenominatorInv * power(fact[count], MOD - 2)) % MOD; } long totalOps = 0; for (int i = 0; i < n; i++) { int remainingChars = n - i; int charCode = s.charAt(i) - 'a'; long countSmaller = bit.query(charCode); if (countSmaller > 0) { long term = (fact[remainingChars - 1] * countSmaller) % MOD; long contribution = (term * permsDenominatorInv) % MOD; totalOps = (totalOps + contribution) % MOD; } permsDenominatorInv = (permsDenominatorInv * freq[charCode]) % MOD; bit.update(charCode + 1, -1); freq[charCode]--; } return (int) totalOps; } private void precomputeFactorials(int n) { fact = new long[n + 1]; fact[0] = 1; for (int i = 1; i <= n; i++) { fact[i] = (fact[i - 1] * i) % MOD; } } private long power(long base, long exp) { long res = 1; base %= MOD; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % MOD; base = (base * base) % MOD; exp /= 2; } return res; } class FenwickTree { int[] bit; int size; public FenwickTree(int size) { this.size = size; this.bit = new int[size + 1]; } public void update(int index, int delta) { while (index <= size) { bit[index] += delta; index += index & -index; } } public int query(int index) { int sum = 0; while (index > 0) { sum += bit[index]; index -= index & -index; } return sum; } }}Complexity
Time
O(N * log(A)), where `N` is the string length and `A` is the alphabet size. Precomputation takes O(N + A*logA).
Space
O(N + A) for factorials and the Fenwick Tree.
Trade-offs
Pros
Most efficient approach.
Reduces the complexity of each step in the main loop from
O(A)toO(log A).
Cons
More complex to implement due to the use of a Fenwick Tree.
The underlying mathematical concept is less direct than simulation.
Solutions
Solution
class Solution { private static final int N = 3010 ; private static final int MOD = ( int ) 1 e9 + 7 ; private static final long [] f = new long [ N ]; private static final long [] g = new long [ N ]; static { f [ 0 ] = 1 ; g [ 0 ] = 1 ; for ( int i = 1 ; i < N ; ++ i ) { f [ i ] = f [ i - 1 ] * i % MOD ; g [ i ] = qmi ( f [ i ], MOD - 2 ); } } public static long qmi ( long a , int k ) { long res = 1 ; while ( k != 0 ) { if (( k & 1 ) == 1 ) { res = res * a % MOD ; } k >>= 1 ; a = a * a % MOD ; } return res ; } public int makeStringSorted ( String s ) { int [] cnt = new int [ 26 ]; int n = s . length (); for ( int i = 0 ; i < n ; ++ i ) { ++ cnt [ s . charAt ( i ) - 'a' ]; } long ans = 0 ; for ( int i = 0 ; i < n ; ++ i ) { int m = 0 ; for ( int j = s . charAt ( i ) - 'a' - 1 ; j >= 0 ; -- j ) { m += cnt [ j ]; } long t = m * f [ n - i - 1 ] % MOD ; for ( int v : cnt ) { t = t * g [ v ] % MOD ; } -- cnt [ s . charAt ( i ) - 'a' ]; ans = ( ans + t + MOD ) % MOD ; } return ( int ) ans ; } }const int N = 3010 ; const int MOD = 1e9 + 7 ; long f [ N ]; long g [ N ]; long qmi ( long a , int k ) { long res = 1 ; while ( k != 0 ) { if (( k & 1 ) == 1 ) { res = res * a % MOD ; } k >>= 1 ; a = a * a % MOD ; } return res ; } int init = []() { f [ 0 ] = g [ 0 ] = 1 ; for ( int i = 1 ; i < N ; ++ i ) { f [ i ] = f [ i - 1 ] * i % MOD ; g [ i ] = qmi ( f [ i ], MOD - 2 ); } return 0 ; }(); class Solution { public: int makeStringSorted ( string s ) { int cnt [ 26 ]{}; for ( char & c : s ) { ++ cnt [ c - 'a' ]; } int n = s . size (); long ans = 0 ; for ( int i = 0 ; i < n ; ++ i ) { int m = 0 ; for ( int j = s [ i ] - 'a' - 1 ; ~ j ; -- j ) { m += cnt [ j ]; } long t = m * f [ n - i - 1 ] % MOD ; for ( int & v : cnt ) { t = t * g [ v ] % MOD ; } ans = ( ans + t + MOD ) % MOD ; -- cnt [ s [ i ] - 'a' ]; } return ans ; } };n = 3010 mod = 10 ** 9 + 7 f = [ 1 ] + [ 0 ] * n g = [ 1 ] + [ 0 ] * n for i in range ( 1 , n ): f [ i ] = f [ i - 1 ] * i % mod g [ i ] = pow ( f [ i ], mod - 2 , mod ) class Solution : def makeStringSorted ( self , s : str ) -> int : cnt = Counter ( s ) ans , n = 0 , len ( s ) for i , c in enumerate ( s ): m = sum ( v for a , v in cnt . items () if a < c ) t = f [ n - i - 1 ] * m for v in cnt . values (): t = t * g [ v ] % mod ans = ( ans + t ) % mod cnt [ c ] -= 1 if cnt [ c ] == 0 : cnt . pop ( c ) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.