1405. Longest Happy String - Explanation

Problem Link

Description

A string s is called happy if it satisfies the following conditions:

  • s only contains the letters 'a', 'b', and 'c'.
  • s does not contain any of "aaa", "bbb", or "ccc" as a substring.
  • s contains at most a occurrences of the letter 'a'.
  • s contains at most b occurrences of the letter 'b'.
  • s contains at most c occurrences of the letter 'c'.

You are given three integers a, b, and c, return the longest possible happy string. If there are multiple longest happy strings, return any of them. If there is no such string, return the empty string "".

A substring is a contiguous sequence of characters within a string.

Example 1:

Input: a = 3, b = 4, c = 2

Output: "bababcabc"

Example 2:

Input: a = 0, b = 1, c = 5

Output: "ccbcc"

Constraints:

  • 0 <= a, b, c <= 100
  • a + b + c > 0


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Greedy Algorithms - Making locally optimal choices (using the most frequent valid character) to build a global solution
  • Heaps (Priority Queues) - Using a max-heap to efficiently retrieve the character with the highest remaining count
  • Recursion - Breaking down the problem into smaller subproblems with reduced character counts

1. Greedy

Intuition

A "happy" string has no three consecutive identical characters. To maximize length, we should greedily use the most abundant character whenever possible. However, if we just used the same character twice in a row, we must switch to a different one to avoid three in a row. By always picking the character with the highest remaining count (that we're allowed to use), we maximize our chances of using all characters.

Algorithm

  1. Track the remaining count of each character (a, b, c).
  2. Track which character was just repeated (to avoid using it a third time).
  3. In each iteration, find the character with the maximum count that is not the repeated one.
  4. Append that character to the result and decrement its count.
  5. If the last two characters are the same, mark that character as repeated; otherwise, reset.
  6. Stop when no valid character can be added.
class Solution:
    def longestDiverseString(self, a: int, b: int, c: int) -> str:
        count = [a, b, c]
        res = []

        def getMax(repeated):
            idx = -1
            maxCnt = 0
            for i in range(3):
                if i == repeated or count[i] == 0:
                    continue
                if maxCnt < count[i]:
                    maxCnt = count[i]
                    idx = i
            return idx

        repeated = -1
        while True:
            maxChar = getMax(repeated)
            if maxChar == -1:
                break
            res.append(chr(maxChar + ord('a')))
            count[maxChar] -= 1
            if len(res) > 1 and res[-1] == res[-2]:
                repeated = maxChar
            else:
                repeated = -1

        return ''.join(res)

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity:
    • O(1)O(1) extra space.
    • O(n)O(n) space for the output string.

2. Greedy (Max-Heap)

Intuition

Using a max-heap streamlines finding the character with the highest count. We pop the top character and try to use it. If doing so would create three in a row, we instead use the second-highest character (if available), then push the first one back. This ensures we always make progress while respecting the constraint.

Algorithm

  1. Push all characters with non-zero counts into a max-heap as (count, char) pairs.
  2. While the heap is not empty:
    • Pop the character with the highest count.
    • If the last two characters in the result match this character, try using the second-highest instead.
    • If no second option exists, stop.
    • Append the chosen character, decrement its count, and push it back if count remains positive.
  3. Return the constructed string.
class Solution:
    def longestDiverseString(self, a: int, b: int, c: int) -> str:
        res = ""
        maxHeap = []
        for count, char in [(-a, "a"), (-b, "b"), (-c, "c")]:
            if count != 0:
                heapq.heappush(maxHeap, (count, char))

        while maxHeap:
            count, char = heapq.heappop(maxHeap)
            if len(res) > 1 and res[-1] == res[-2] == char:
                if not maxHeap:
                    break
                count2, char2 = heapq.heappop(maxHeap)
                res += char2
                count2 += 1
                if count2:
                    heapq.heappush(maxHeap, (count2, char2))
                heapq.heappush(maxHeap, (count, char))
            else:
                res += char
                count += 1
                if count:
                    heapq.heappush(maxHeap, (count, char))

        return res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity:
    • O(1)O(1) extra space.
    • O(n)O(n) space for the output string.

3. Greedy (Recursion)

Intuition

We can express the greedy logic recursively. At each step, we sort the characters by count (keeping the largest first) and decide how many of the most frequent character to use. If the most frequent character still dominates after using two of it, we insert one of the second character to break it up. Then we recurse on the reduced counts.

Algorithm

  1. Define a recursive function that takes counts and characters for all three options.
  2. Reorder parameters so the first character has the highest count.
  3. Base case: If the second count is zero, return up to two of the first character.
  4. Use up to two of the first character. If the remaining count still exceeds the second, use one of the second character.
  5. Recurse with updated counts and concatenate the results.
class Solution:
    def longestDiverseString(self, a: int, b: int, c: int) -> str:
        def rec(max1, max2, max3, char1, char2, char3):
            if max1 < max2:
                return rec(max2, max1, max3, char2, char1, char3)
            if max2 < max3:
                return rec(max1, max3, max2, char1, char3, char2)
            if max2 == 0:
                return [char1] * min(2, max1)

            use1 = min(2, max1)
            use2 = 1 if max1 - use1 >= max2 else 0
            res = [char1] * use1 + [char2] * use2
            return res + rec(max1 - use1, max2 - use2, max3, char1, char2, char3)

        return ''.join(rec(a, b, c, 'a', 'b', 'c'))

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity:
    • O(n)O(n) for recursion stack.
    • O(n)O(n) space for the output string.

Common Pitfalls

Forgetting to Track Consecutive Characters

A common mistake is only checking whether the last character matches the current one. The problem requires preventing three consecutive identical characters, not just two. You must track whether the last two characters are the same before deciding if you can add another of the same character.

Greedily Using the Same Character Without Switching

Always picking the most frequent character without considering when to switch leads to suboptimal or invalid results. When you have used the most frequent character twice in a row, you must use a different character next, even if the most frequent one still has remaining count. Failing to switch results in either an invalid string or a shorter result than possible.

Not Handling the Case When No Valid Character Exists

The algorithm must correctly terminate when no valid character can be added. This happens when all remaining characters would create three consecutive identical characters. Forgetting to handle this edge case can lead to infinite loops or incorrect outputs.