Facebook Pixel

2182. Construct String With Repeat Limit

MediumGreedyHash TableStringCountingHeap (Priority Queue)
Leetcode Link

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 string b if at the first position where they differ, string a has a letter that comes later in the alphabet than the corresponding letter in b.
  • If the first min(a.length, b.length) characters are identical, then the longer string is considered lexicographically larger.

Example understanding:

  • If s = "cczazcc" and repeatLimit = 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Count frequency of each character
  2. Start from the largest character (index 25 for 'z')
  3. Use as many of this character as allowed (up to repeatLimit or until exhausted)
  4. If we still have more of this character, find the next largest available character to use as a breaker
  5. Use exactly one instance of the breaker character
  6. 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:

  1. Two-pointer technique: Variable i tracks the current largest character we're trying to use, while j tracks the next largest available character to use as a breaker.

  2. Greedy selection: We always take min(repeatLimit, cnt[i]) characters at once, maximizing the use of larger characters.

  3. Breaker optimization: We only use one character as a breaker each time, allowing us to return to the larger character immediately.

  4. Early termination: If no breaker character is available (j < 0), we stop even if we have remaining characters of type i, 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 Evaluator

Example 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:

  1. We used the largest available character ('z') first
  2. When we hit the repeat limit for 'c', we used the next available character ('a') as a minimal breaker
  3. 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) where n is the length of string s
  • 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 most cnt[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 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More