2182. Construct String With Repeat Limit
Problem Description
You are given a string s
and an integer repeatLimit
. Your task is to construct a new string called repeatLimitedString
using the characters from s
, with the constraint that no letter can appear more than repeatLimit
times consecutively. You don't need to use all the characters from s
.
The goal is to return the lexicographically largest possible repeatLimitedString
.
What makes a string lexicographically larger?
- A string
a
is lexicographically larger than stringb
if at the first position where they differ, stringa
has a letter that comes later in the alphabet than the corresponding letter inb
. - If the first
min(a.length, b.length)
characters are identical, then the longer string is considered lexicographically larger.
Example understanding:
- If
s = "cczazcc"
andrepeatLimit = 3
, you need to rearrange the characters to create the largest possible string where no character repeats more than 3 times in a row. - Characters available: c(4), z(2), a(1)
- To maximize lexicographical value, you'd want to use 'z' first, then 'c', then 'a'
- But you can't use more than
repeatLimit
consecutive occurrences of any character
The strategy involves greedily selecting the largest available character while respecting the repeat limit constraint. When you hit the limit for a character, you need to insert a different character before continuing with the original character.
Intuition
To create the lexicographically largest string, we want to use characters in descending order (z, y, x, ... , a) as much as possible. The greedy approach would be to always pick the largest available character.
However, we're constrained by the repeatLimit
. When we've used a character repeatLimit
times consecutively, we must "break" the sequence by inserting a different character before we can use the original character again.
Here's the key insight: When we need to break a sequence of the largest character, we should use the next largest available character as the "breaker" - but only one instance of it. Why? Because we want to get back to using the largest character as soon as possible.
Think of it like this:
- We have character counts stored (let's say 'z' appears 7 times, 'x' appears 2 times, and
repeatLimit = 3
) - We use 'z' three times: "zzz"
- We still have 4 'z's left, but we can't use them consecutively
- We insert one 'x' as a breaker: "zzzx"
- Now we can use 'z' again for up to 3 more times: "zzzxzzz"
- We insert another 'x': "zzzxzzzx"
- Finally, we use the last 'z': "zzzxzzzxz"
The algorithm naturally falls out from this observation:
- Count frequency of each character
- Start from the largest character (index 25 for 'z')
- Use as many of this character as allowed (up to
repeatLimit
or until exhausted) - If we still have more of this character, find the next largest available character to use as a breaker
- Use exactly one instance of the breaker character
- Repeat until we can't find any breaker characters or all characters are used
This greedy strategy ensures we always maintain the lexicographically largest possible string at each step.
Learn more about Greedy and Heap (Priority Queue) patterns.
Solution Approach
The implementation follows the greedy strategy we identified:
Step 1: Character Frequency Counting
cnt = [0] * 26
for c in s:
cnt[ord(c) - ord("a")] += 1
We create an array cnt
of size 26 to store the frequency of each character. Each index represents a letter ('a' at index 0, 'b' at index 1, ..., 'z' at index 25).
Step 2: Build the Result String We iterate through characters from largest to smallest (index 25 to 0):
ans = []
j = 24 # Pointer for the "breaker" character
for i in range(25, -1, -1):
j = min(i - 1, j) # Ensure j is always less than i
Step 3: Main Logic for Each Character
For each character at index i
, we use a while loop to handle all occurrences:
while 1:
x = min(repeatLimit, cnt[i]) # Take at most repeatLimit characters
cnt[i] -= x
ans.append(ascii_lowercase[i] * x)
if cnt[i] == 0: # All characters used, move to next
break
Step 4: Handle the Breaker Character
When we still have characters left after using repeatLimit
of them, we need a breaker:
while j >= 0 and cnt[j] == 0: j -= 1 # Find the next available smaller character if j < 0: # No breaker available break cnt[j] -= 1 ans.append(ascii_lowercase[j]) # Use exactly one breaker
Key Implementation Details:
-
Two-pointer technique: Variable
i
tracks the current largest character we're trying to use, whilej
tracks the next largest available character to use as a breaker. -
Greedy selection: We always take
min(repeatLimit, cnt[i])
characters at once, maximizing the use of larger characters. -
Breaker optimization: We only use one character as a breaker each time, allowing us to return to the larger character immediately.
-
Early termination: If no breaker character is available (
j < 0
), we stop even if we have remaining characters of typei
, as we cannot use them without violating the repeat limit.
The final result is constructed by joining all the character segments: "".join(ans)
.
This approach ensures O(n) time complexity where n is the length of the input string, as we process each character once during counting and once during construction.
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 a concrete example with s = "cczazcc"
and repeatLimit = 2
.
Step 1: Count character frequencies
- c: 4 occurrences
- z: 2 occurrences
- a: 1 occurrence
- Array representation:
cnt = [1, 0, 4, ..., 0, 2]
(indices 0='a', 2='c', 25='z')
Step 2: Process characters from largest to smallest
Starting with i=25 (character 'z'), j=24:
- We have 2 'z's available
- Take
min(2, 2) = 2
characters - Result:
"zz"
- cnt[25] = 0 (all 'z's used)
- Move to next character
Moving to i=2 (character 'c'), j=1:
- We have 4 'c's available
- Take
min(2, 4) = 2
characters (repeatLimit is 2) - Result:
"zzcc"
- cnt[2] = 2 (2 'c's remaining)
- Still have 'c's left, need a breaker!
Finding breaker character:
- j starts at 1, check cnt[1]=0 (no 'b'), move j to 0
- cnt[0]=1 (we have 'a'!)
- Use 1 'a' as breaker
- Result:
"zzcca"
- cnt[0] = 0
Continue with 'c':
- Take
min(2, 2) = 2
remaining 'c's - Result:
"zzccacc"
- cnt[2] = 0 (all 'c's used)
Final Result: "zzccacc"
This is lexicographically largest because:
- We used the largest available character ('z') first
- When we hit the repeat limit for 'c', we used the next available character ('a') as a minimal breaker
- We maximized the use of larger characters while respecting the consecutive repeat constraint
Solution Implementation
1class Solution:
2 def repeatLimitedString(self, s: str, repeatLimit: int) -> str:
3 """
4 Construct the lexicographically largest string from characters in s
5 where no character appears more than repeatLimit times consecutively.
6
7 Args:
8 s: Input string containing lowercase English letters
9 repeatLimit: Maximum consecutive occurrences allowed for any character
10
11 Returns:
12 The lexicographically largest valid string
13 """
14 # Count frequency of each character (a-z)
15 char_count = [0] * 26
16 for char in s:
17 char_count[ord(char) - ord('a')] += 1
18
19 result = []
20
21 # Index for the next smaller character to use as separator
22 separator_idx = 24
23
24 # Process characters from largest (z) to smallest (a)
25 for current_idx in range(25, -1, -1):
26 # Update separator index to be at most one less than current
27 separator_idx = min(current_idx - 1, separator_idx)
28
29 # Keep using current character until exhausted
30 while True:
31 # Add as many of current character as allowed (up to repeatLimit)
32 chars_to_add = min(repeatLimit, char_count[current_idx])
33 char_count[current_idx] -= chars_to_add
34 result.append(chr(ord('a') + current_idx) * chars_to_add)
35
36 # If current character is exhausted, move to next
37 if char_count[current_idx] == 0:
38 break
39
40 # Find next available smaller character to use as separator
41 while separator_idx >= 0 and char_count[separator_idx] == 0:
42 separator_idx -= 1
43
44 # If no separator available, cannot place more of current character
45 if separator_idx < 0:
46 break
47
48 # Use one instance of separator character to break the sequence
49 char_count[separator_idx] -= 1
50 result.append(chr(ord('a') + separator_idx))
51
52 return ''.join(result)
53
1class Solution {
2 public String repeatLimitedString(String s, int repeatLimit) {
3 // Count frequency of each character (a-z)
4 int[] charCount = new int[26];
5 for (int i = 0; i < s.length(); i++) {
6 charCount[s.charAt(i) - 'a']++;
7 }
8
9 StringBuilder result = new StringBuilder();
10
11 // Process characters from 'z' to 'a' (largest to smallest)
12 for (int currentIndex = 25, nextIndex = 24; currentIndex >= 0; currentIndex--) {
13 // Ensure nextIndex is always smaller than currentIndex
14 nextIndex = Math.min(nextIndex, currentIndex - 1);
15
16 while (true) {
17 // Append current character up to repeatLimit times or until exhausted
18 int appendCount = Math.min(charCount[currentIndex], repeatLimit);
19 for (int k = 0; k < appendCount; k++) {
20 result.append((char) ('a' + currentIndex));
21 charCount[currentIndex]--;
22 }
23
24 // If current character is exhausted, move to next character
25 if (charCount[currentIndex] == 0) {
26 break;
27 }
28
29 // Find the next available smaller character
30 while (nextIndex >= 0 && charCount[nextIndex] == 0) {
31 nextIndex--;
32 }
33
34 // If no smaller character available, we cannot continue
35 if (nextIndex < 0) {
36 break;
37 }
38
39 // Append one instance of the smaller character as separator
40 result.append((char) ('a' + nextIndex));
41 charCount[nextIndex]--;
42 }
43 }
44
45 return result.toString();
46 }
47}
48
1class Solution {
2public:
3 string repeatLimitedString(string s, int repeatLimit) {
4 // Count frequency of each character (a-z)
5 int charCount[26]{};
6 for (char& c : s) {
7 ++charCount[c - 'a'];
8 }
9
10 string result;
11
12 // Process characters from 'z' to 'a' (largest to smallest)
13 for (int currentIdx = 25, nextIdx = 24; currentIdx >= 0; --currentIdx) {
14 // Ensure nextIdx is always less than currentIdx
15 nextIdx = min(nextIdx, currentIdx - 1);
16
17 // Keep processing the current character until it's exhausted
18 while (true) {
19 // Add up to repeatLimit occurrences of current character
20 int charsToAdd = min(charCount[currentIdx], repeatLimit);
21 for (int k = 0; k < charsToAdd; ++k) {
22 result += static_cast<char>('a' + currentIdx);
23 --charCount[currentIdx];
24 }
25
26 // If current character is exhausted, move to next character
27 if (charCount[currentIdx] == 0) {
28 break;
29 }
30
31 // Find the next available smaller character to break the sequence
32 while (nextIdx >= 0 && charCount[nextIdx] == 0) {
33 --nextIdx;
34 }
35
36 // If no smaller character available, we can't continue
37 if (nextIdx < 0) {
38 break;
39 }
40
41 // Add one occurrence of the next smaller character as separator
42 result += static_cast<char>('a' + nextIdx);
43 --charCount[nextIdx];
44 }
45 }
46
47 return result;
48 }
49};
50
1/**
2 * Constructs the lexicographically largest string with a repeat limit constraint
3 * @param s - Input string to reconstruct
4 * @param repeatLimit - Maximum consecutive occurrences allowed for any character
5 * @returns The lexicographically largest valid string
6 */
7function repeatLimitedString(s: string, repeatLimit: number): string {
8 // Count frequency of each character (a-z mapped to indices 0-25)
9 const charFrequency: number[] = Array(26).fill(0);
10 for (const char of s) {
11 charFrequency[char.charCodeAt(0) - 97]++;
12 }
13
14 // Build result string character by character
15 const result: string[] = [];
16
17 // Process characters from z to a (indices 25 to 0) for lexicographically largest result
18 for (let currentIndex = 25, nextAvailableIndex = 24; currentIndex >= 0; currentIndex--) {
19 // Ensure nextAvailableIndex is always less than currentIndex
20 nextAvailableIndex = Math.min(nextAvailableIndex, currentIndex - 1);
21
22 while (true) {
23 // Add up to repeatLimit occurrences of current character
24 const charsToAdd = Math.min(charFrequency[currentIndex], repeatLimit);
25 for (let k = 0; k < charsToAdd; k++) {
26 result.push(String.fromCharCode(97 + currentIndex));
27 charFrequency[currentIndex]--;
28 }
29
30 // If current character is exhausted, move to next character
31 if (charFrequency[currentIndex] === 0) {
32 break;
33 }
34
35 // Find next available character to use as separator
36 while (nextAvailableIndex >= 0 && charFrequency[nextAvailableIndex] === 0) {
37 nextAvailableIndex--;
38 }
39
40 // No separator available, cannot place remaining characters
41 if (nextAvailableIndex < 0) {
42 break;
43 }
44
45 // Add one occurrence of separator character to break the sequence
46 result.push(String.fromCharCode(97 + nextAvailableIndex));
47 charFrequency[nextAvailableIndex]--;
48 }
49 }
50
51 return result.join('');
52}
53
Time and Space Complexity
Time Complexity: O(n + |Σ|)
The algorithm consists of several parts:
- Counting character frequencies:
O(n)
wheren
is the length of strings
- The main loop iterates through all 26 letters from 'z' to 'a':
O(|Σ|)
where|Σ| = 26
- Inside the main loop:
- The inner while loop for each character
i
runs at mostcnt[i]
times in total - The inner while loop searching for valid
j
moves through the alphabet at most once overall - String concatenation operations append characters proportional to the input size
- The inner while loop for each character
- The total number of characters appended equals
n
(the original string length)
Since we process each character from the input exactly once and iterate through the alphabet a constant number of times, the overall time complexity is O(n + |Σ|)
where |Σ| = 26
.
Space Complexity: O(|Σ|)
The algorithm uses:
cnt
array of size 26 to store character frequencies:O(|Σ|)
ans
list to build the result string:O(n)
in the worst case, but this is typically not counted as extra space since it's the output- A few integer variables (
i
,j
,x
):O(1)
The dominant extra space used (excluding the output) is the frequency array, giving us a space complexity of O(|Σ|)
where |Σ| = 26
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Inefficient Separator Character Selection
Issue: A common mistake is resetting the separator search from the beginning (index 24 or current_idx - 1) every time we need a breaker character. This leads to unnecessary iterations through already exhausted characters.
Wrong Approach:
# Inefficient - starts search from scratch each time
for current_idx in range(25, -1, -1):
while char_count[current_idx] > 0:
# ... add characters ...
if char_count[current_idx] > 0:
# Wrong: Always starting from current_idx - 1
for j in range(current_idx - 1, -1, -1):
if char_count[j] > 0:
char_count[j] -= 1
result.append(chr(ord('a') + j))
break
Solution: Maintain a persistent separator pointer that doesn't reset, only moving backward when necessary. This ensures O(26) total iterations for finding separators across the entire algorithm.
Pitfall 2: Using Multiple Separator Characters
Issue: When breaking a sequence, using more than one separator character at a time wastes larger characters that could be used later in blocks.
Wrong Approach:
# Using too many separators reduces the final string's lexicographical value
if char_count[current_idx] > 0:
# Wrong: Using multiple separators unnecessarily
separators_needed = min(char_count[separator_idx], 2) # Or any number > 1
char_count[separator_idx] -= separators_needed
result.append(chr(ord('a') + separator_idx) * separators_needed)
Solution: Always use exactly ONE separator character. This maximizes the opportunity to return to using the larger character immediately after the break.
Pitfall 3: Not Handling Early Termination Properly
Issue: Continuing to process when no valid separator exists can lead to violations of the repeat limit or infinite loops.
Wrong Approach:
while char_count[current_idx] > 0:
chars_to_add = min(repeatLimit, char_count[current_idx])
char_count[current_idx] -= chars_to_add
result.append(chr(ord('a') + current_idx) * chars_to_add)
# Wrong: Not checking if separator exists before trying to use current character again
if char_count[current_idx] > 0:
# Might not find any separator but continues anyway
char_count[separator_idx] -= 1 # Could be accessing invalid index
result.append(chr(ord('a') + separator_idx))
Solution: Always check if a valid separator exists (separator_idx >= 0
) before attempting to continue with the current character. If no separator is available, break from the loop even if characters remain unused.
Pitfall 4: Incorrect Separator Index Initialization
Issue: Setting the separator index incorrectly can lead to attempting to use a character larger than or equal to the current character as a separator.
Wrong Approach:
for current_idx in range(25, -1, -1):
# Wrong: separator could be >= current_idx
separator_idx = 25 # Or not updating it at all
Solution: Always ensure separator_idx = min(current_idx - 1, separator_idx)
at the start of processing each character. This guarantees the separator is lexicographically smaller than the current character being processed.
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
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!