Facebook Pixel

1405. Longest Happy String

Problem Description

You need to create the longest possible "happy" string using the letters 'a', 'b', and 'c' with specific constraints.

A string is considered happy if it meets all of these requirements:

  • It only contains the letters 'a', 'b', and 'c'
  • It doesn't have three consecutive identical characters (no "aaa", "bbb", or "ccc" as substrings)
  • It uses at most a occurrences of the letter 'a'
  • It uses at most b occurrences of the letter 'b'
  • It uses at most c occurrences of the letter 'c'

Given three integers a, b, and c representing the maximum number of times each letter can appear, you need to return the longest possible happy string. If multiple valid answers exist with the same maximum length, you can return any of them. If no valid happy string can be formed, return an empty string "".

For example, if a = 1, b = 1, c = 7, a valid happy string could be "ccaccbcc" or "ccbccacc". Both use at most 1 'a', at most 1 'b', and at most 7 'c's, while avoiding three consecutive identical characters.

The key challenge is to strategically arrange the characters to maximize the string length while preventing any letter from appearing three times in a row.

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

Intuition

The key insight is that we want to use up all available characters while avoiding three consecutive identical characters. To maximize the string length, we should prioritize using characters that have the most remaining count - this prevents us from getting stuck with many leftover characters that we can't use.

Think of it like distributing items: if you have 10 of item A and 2 of item B, and you need to alternate between them, you should use A more frequently early on. If you use them equally at first, you'll run out of B quickly and be left with many unused As.

The strategy becomes: always pick the character with the highest remaining count. However, there's a catch - if picking that character would create three in a row (when the last two characters in our result are the same as the one we want to pick), we need to pick the character with the second-highest count instead.

This naturally leads to using a max heap (priority queue) where we can:

  1. Quickly get the character with the most remaining occurrences
  2. When needed, temporarily set aside the most frequent character and pick the second most frequent one
  3. Keep track of counts dynamically as we build the string

The process continues until we can't add any more characters without violating the "no three in a row" rule. At each step, we're making the locally optimal choice (greedy approach) of using the most abundant character available, which leads to the globally optimal solution of the longest possible happy string.

Learn more about Greedy and Heap (Priority Queue) patterns.

Solution Approach

The solution uses a greedy approach with a max heap (priority queue) to always select the character with the most remaining occurrences while avoiding three consecutive identical characters.

Step-by-step implementation:

  1. Initialize the max heap: Create a max heap to store character counts. Since Python's heapq implements a min heap, we store negative counts to simulate a max heap.

    h = []
    if a > 0: heappush(h, [-a, 'a'])
    if b > 0: heappush(h, [-b, 'b'])
    if c > 0: heappush(h, [-c, 'c'])
  2. Build the string greedily: Process characters from the heap until it's empty:

    a. Pop the character with highest count: cur = heappop(h)

    b. Check for consecutive characters: If the last two characters in our result are the same as the current character:

    • We can't use this character (would create three in a row)
    • If no other characters available, break
    • Otherwise, pop the next highest character: nxt = heappop(h)
    • Append this alternative character to result
    • Decrement its count and push back to heap if count > 1
    • Push the original character back unchanged

    c. Safe to use current character: If we won't create three in a row:

    • Append the character to result
    • Decrement its count and push back to heap if count > 1
  3. Return the result: Join all characters to form the final string.

Key algorithm details:

  • The heap maintains characters sorted by remaining count (highest first)
  • We use negative values because Python's heapq is a min heap: [-count, character]
  • After using a character, we increment the negative count (cur[0] += 1) and push it back if more instances remain
  • The check len(ans) >= 2 and ans[-1] == cur[1] and ans[-2] == cur[1] ensures we don't create three consecutive identical characters

This greedy strategy ensures we use characters optimally - always picking the most abundant available character that won't violate the constraints, resulting in the longest possible happy string.

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 the solution with a = 1, b = 1, c = 4.

Step 1: Initialize the max heap

  • Push all non-zero counts as negative values (to simulate max heap)
  • Heap: [[-4, 'c'], [-1, 'a'], [-1, 'b']]
  • Result: ""

Step 2: First iteration

  • Pop [-4, 'c'] (highest count)
  • Result is empty, so no risk of three consecutive
  • Append 'c' to result: "c"
  • Decrement count: -4 + 1 = -3, push back [-3, 'c']
  • Heap: [[-3, 'c'], [-1, 'a'], [-1, 'b']]

Step 3: Second iteration

  • Pop [-3, 'c']
  • Last char is 'c', but we don't have two consecutive yet
  • Append 'c' to result: "cc"
  • Decrement count: -3 + 1 = -2, push back [-2, 'c']
  • Heap: [[-2, 'c'], [-1, 'a'], [-1, 'b']]

Step 4: Third iteration

  • Pop [-2, 'c']
  • Last two chars are "cc" - can't use 'c' (would make "ccc")
  • Pop next highest: [-1, 'a']
  • Append 'a' to result: "cca"
  • Count becomes 0, don't push 'a' back
  • Push [-2, 'c'] back unchanged
  • Heap: [[-2, 'c'], [-1, 'b']]

Step 5: Fourth iteration

  • Pop [-2, 'c']
  • Last two chars are "ca" - safe to use 'c'
  • Append 'c' to result: "ccac"
  • Decrement count: -2 + 1 = -1, push back [-1, 'c']
  • Heap: [[-1, 'b'], [-1, 'c']]

Step 6: Fifth iteration

  • Pop [-1, 'c'] (after heap rebalancing)
  • Last two chars are "ac" - safe to use 'c'
  • Append 'c' to result: "ccacc"
  • Count becomes 0, don't push back
  • Heap: [[-1, 'b']]

Step 7: Sixth iteration

  • Pop [-1, 'b']
  • Last two chars are "cc" - can't use 'b' (but wait, 'b' ≠ 'c', so it's safe!)
  • Append 'b' to result: "ccaccb"
  • Count becomes 0, don't push back
  • Heap: []

Final Result: "ccaccb"

The algorithm successfully used all 6 characters (1 'a', 1 'b', 4 'c's) while avoiding any substring of three consecutive identical characters. The greedy approach of always using the most abundant character (when safe) ensured we didn't get stuck with unused characters.

Solution Implementation

1class Solution:
2    def longestDiverseString(self, a: int, b: int, c: int) -> str:
3        from heapq import heappush, heappop
4      
5        # Initialize max heap (using negative values for max heap behavior)
6        # Store tuples of (-count, character)
7        max_heap = []
8      
9        # Add characters to heap if they have positive count
10        if a > 0:
11            heappush(max_heap, [-a, 'a'])
12        if b > 0:
13            heappush(max_heap, [-b, 'b'])
14        if c > 0:
15            heappush(max_heap, [-c, 'c'])
16      
17        # Build result string
18        result = []
19      
20        while max_heap:
21            # Get character with highest remaining count
22            first_char = heappop(max_heap)
23          
24            # Check if adding this character would create three consecutive same characters
25            if len(result) >= 2 and result[-1] == first_char[1] and result[-2] == first_char[1]:
26                # Cannot use the most frequent character, need to use second most frequent
27                if not max_heap:
28                    # No other character available, stop building string
29                    break
30              
31                # Use second most frequent character instead
32                second_char = heappop(max_heap)
33                result.append(second_char[1])
34              
35                # Decrement count and push back if still has remaining count
36                if -second_char[0] > 1:
37                    second_char[0] += 1  # Increment negative value (decrements actual count)
38                    heappush(max_heap, second_char)
39              
40                # Push the first character back to heap for future use
41                heappush(max_heap, first_char)
42            else:
43                # Safe to use the most frequent character
44                result.append(first_char[1])
45              
46                # Decrement count and push back if still has remaining count
47                if -first_char[0] > 1:
48                    first_char[0] += 1  # Increment negative value (decrements actual count)
49                    heappush(max_heap, first_char)
50      
51        # Join list of characters into final string
52        return ''.join(result)
53
1class Solution {
2    public String longestDiverseString(int a, int b, int c) {
3        // Create a max heap to store characters with their counts
4        // Priority is based on count (higher count has higher priority)
5        Queue<int[]> maxHeap = new PriorityQueue<>((x, y) -> y[1] - x[1]);
6      
7        // Add characters to heap if they have positive count
8        if (a > 0) {
9            maxHeap.offer(new int[] {'a', a});
10        }
11        if (b > 0) {
12            maxHeap.offer(new int[] {'b', b});
13        }
14        if (c > 0) {
15            maxHeap.offer(new int[] {'c', c});
16        }
17
18        StringBuilder result = new StringBuilder();
19      
20        // Build the string greedily using characters with highest remaining count
21        while (!maxHeap.isEmpty()) {
22            // Get the character with highest count
23            int[] mostFrequent = maxHeap.poll();
24            int resultLength = result.length();
25          
26            // Check if adding this character would create three consecutive same characters
27            if (resultLength >= 2 && 
28                result.codePointAt(resultLength - 1) == mostFrequent[0] && 
29                result.codePointAt(resultLength - 2) == mostFrequent[0]) {
30              
31                // Cannot use the most frequent character, need to use second most frequent
32                if (maxHeap.isEmpty()) {
33                    // No other character available, stop building
34                    break;
35                }
36              
37                // Use the second most frequent character instead
38                int[] secondMostFrequent = maxHeap.poll();
39                result.append((char) secondMostFrequent[0]);
40              
41                // Put back the second most frequent if it still has remaining count
42                if (secondMostFrequent[1] > 1) {
43                    secondMostFrequent[1]--;
44                    maxHeap.offer(secondMostFrequent);
45                }
46              
47                // Put back the most frequent character for future use
48                maxHeap.offer(mostFrequent);
49            } else {
50                // Safe to use the most frequent character
51                result.append((char) mostFrequent[0]);
52              
53                // Put back if there are still remaining instances
54                if (mostFrequent[1] > 1) {
55                    mostFrequent[1]--;
56                    maxHeap.offer(mostFrequent);
57                }
58            }
59        }
60
61        return result.toString();
62    }
63}
64
1class Solution {
2public:
3    string longestDiverseString(int a, int b, int c) {
4        // Define pair type: character and its remaining count
5        using CharCountPair = pair<char, int>;
6      
7        // Comparator for max heap based on count
8        auto comparator = [](CharCountPair x, CharCountPair y) { 
9            return x.second < y.second; 
10        };
11      
12        // Max heap to store characters with their counts (prioritized by count)
13        priority_queue<CharCountPair, vector<CharCountPair>, decltype(comparator)> maxHeap(comparator);
14
15        // Add characters to heap if they have positive count
16        if (a > 0) maxHeap.push({'a', a});
17        if (b > 0) maxHeap.push({'b', b});
18        if (c > 0) maxHeap.push({'c', c});
19
20        string result;
21      
22        // Greedy approach: always try to use the character with highest remaining count
23        while (!maxHeap.empty()) {
24            // Get the character with highest count
25            CharCountPair mostFrequent = maxHeap.top();
26            maxHeap.pop();
27          
28            int resultLength = result.size();
29          
30            // Check if adding this character would create three consecutive same characters
31            if (resultLength >= 2 && 
32                result[resultLength - 1] == mostFrequent.first && 
33                result[resultLength - 2] == mostFrequent.first) {
34              
35                // Cannot use the most frequent character, need to use the second most frequent
36                if (maxHeap.empty()) {
37                    break;  // No other character available, terminate
38                }
39              
40                // Use the second most frequent character instead
41                CharCountPair secondMostFrequent = maxHeap.top();
42                maxHeap.pop();
43              
44                result.push_back(secondMostFrequent.first);
45              
46                // Put back the character if it still has remaining count
47                if (--secondMostFrequent.second > 0) {
48                    maxHeap.push(secondMostFrequent);
49                }
50              
51                // Put back the most frequent character for future use
52                maxHeap.push(mostFrequent);
53            } else {
54                // Safe to use the most frequent character
55                result.push_back(mostFrequent.first);
56              
57                // Put back the character if it still has remaining count
58                if (--mostFrequent.second > 0) {
59                    maxHeap.push(mostFrequent);
60                }
61            }
62        }
63
64        return result;
65    }
66};
67
1/**
2 * Generates the longest string containing 'a', 'b', and 'c' characters
3 * where no character appears more than twice consecutively
4 * @param a - Count of 'a' characters available
5 * @param b - Count of 'b' characters available
6 * @param c - Count of 'c' characters available
7 * @returns The longest valid diverse string
8 */
9function longestDiverseString(a: number, b: number, c: number): string {
10    // Array to build the result string character by character
11    const result: string[] = [];
12  
13    // Store character-count pairs for processing
14    // Each tuple contains [character, remaining count]
15    const characterCounts: [string, number][] = [
16        ['a', a],
17        ['b', b],
18        ['c', c],
19    ];
20  
21    // Continue building string until no valid characters can be added
22    while (true) {
23        // Sort characters by remaining count in descending order
24        // This ensures we prioritize characters with higher counts
25        characterCounts.sort((first, second) => second[1] - first[1]);
26      
27        // Flag to track if we successfully added a character in this iteration
28        let addedCharacter = false;
29      
30        // Try to add a character from the sorted list
31        for (let index = 0; index < characterCounts.length; index++) {
32            const [character, remainingCount] = characterCounts[index];
33          
34            // Skip if no more of this character available
35            if (remainingCount < 1) {
36                break;
37            }
38          
39            // Check if adding this character would create three consecutive same characters
40            const resultLength = result.length;
41            if (resultLength >= 2 && 
42                result[resultLength - 1] === character && 
43                result[resultLength - 2] === character) {
44                // Skip this character and try the next one
45                continue;
46            }
47          
48            // Valid character found - add it to result
49            addedCharacter = true;
50            result.push(character);
51            characterCounts[index][1] -= 1;
52            break;
53        }
54      
55        // Exit loop if no valid character could be added
56        if (!addedCharacter) {
57            break;
58        }
59    }
60  
61    // Join the character array into final string
62    return result.join('');
63}
64

Time and Space Complexity

Time Complexity: O((a + b + c) * log 3)

The algorithm uses a max heap to greedily select characters. The heap contains at most 3 elements (one for each character 'a', 'b', 'c'). The main while loop runs for at most a + b + c iterations since each iteration adds exactly one character to the result string and decrements one counter. In each iteration, we perform at most 2 heap pop operations and 2 heap push operations, each taking O(log 3) time. Since log 3 is a constant, the time complexity simplifies to O(a + b + c).

Space Complexity: O(a + b + c)

The space complexity consists of:

  • The heap h which stores at most 3 elements: O(1)
  • The answer list ans which stores the final string of length at most a + b + c: O(a + b + c)
  • The final string created by ''.join(ans): O(a + b + c)

Therefore, the overall space complexity is O(a + b + c).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Using Enough Characters When Possible

A major pitfall is being too conservative and only adding one character at a time, which can lead to suboptimal string lengths.

Problem Example: With a=0, b=2, c=1, if we always add just one character:

  • Result: "bcb" or "cbc" (length 3)
  • Optimal: "bbc" or "cbb" (length 3 - same in this case but approach matters)

With larger differences like a=1, b=1, c=7:

  • Suboptimal approach might give: "cbcbccc" (length 7)
  • Better approach: "ccbccacc" or "ccaccbcc" (length 8)

Solution: When the most frequent character has significantly more occurrences than others, add two characters at once (when safe):

def longestDiverseString(self, a: int, b: int, c: int) -> str:
    from heapq import heappush, heappop
  
    max_heap = []
    if a > 0: heappush(max_heap, [-a, 'a'])
    if b > 0: heappush(max_heap, [-b, 'b'])
    if c > 0: heappush(max_heap, [-c, 'c'])
  
    result = []
  
    while max_heap:
        first = heappop(max_heap)
      
        # Check if we need to use a different character
        if len(result) >= 2 and result[-1] == first[1] and result[-2] == first[1]:
            if not max_heap:
                break
            second = heappop(max_heap)
            result.append(second[1])
            if -second[0] > 1:
                second[0] += 1
                heappush(max_heap, second)
            heappush(max_heap, first)
        else:
            # KEY IMPROVEMENT: Add two characters if:
            # 1. We have at least 2 of this character remaining
            # 2. Either result is empty, last char is different, or we only have one of the last char
            # 3. This character has more remaining than others (or is the only one left)
            use_two = False
            if -first[0] >= 2:
                if len(result) == 0 or result[-1] != first[1]:
                    # Safe to add two if this char has more than others
                    if not max_heap or -first[0] > -max_heap[0][0]:
                        use_two = True
          
            if use_two:
                result.append(first[1])
                result.append(first[1])
                first[0] += 2  # Decrement by 2
            else:
                result.append(first[1])
                first[0] += 1  # Decrement by 1
          
            if -first[0] > 0:
                heappush(max_heap, first)
  
    return ''.join(result)

2. Incorrect Three-Consecutive Check

Problem: Checking only the last character instead of the last two characters:

# WRONG: Only checks if last char matches
if len(result) >= 1 and result[-1] == first[1]:
    # This would prevent "aa" which is valid!

Solution: Always check both of the last two characters:

# CORRECT: Checks if last TWO chars match current
if len(result) >= 2 and result[-1] == first[1] and result[-2] == first[1]:
    # This correctly prevents "aaa" but allows "aa"

3. Not Handling Edge Cases with Empty Heap

Problem: When trying to use an alternate character but the heap is empty:

# Missing the empty heap check could cause IndexError
second_char = heappop(max_heap)  # Crashes if heap is empty!

Solution: Always check if alternate characters are available:

if not max_heap:
    break  # No alternative available, must stop
second_char = heappop(max_heap)

These improvements ensure the algorithm produces truly optimal results while avoiding runtime errors.

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

Which of the following is a min heap?


Recommended Readings

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

Load More