title: Longest Happy String slug: longest-happy-string difficulty: medium leetcode_id: 1405 leetcode_url: https://leetcode.com/problems/longest-happy-string/ categories: - strings - heap patterns: - greedy - heap 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'`. 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. constraints: | - `0 <= a, b, c <= 100` - `a + b + c > 0` examples: - input: "a = 1, b = 1, c = 7" output: '"ccaccbcc"' explanation: '"ccbccacc" would also be a correct answer.' - input: "a = 7, b = 1, c = 0" output: '"aabaa"' explanation: "It is the only correct answer in this case." explanation: intuition: | Imagine you're filling a jar with coloured marbles (a, b, c), but you have a rule: **no more than two marbles of the same colour can sit adjacent to each other**. The key insight is that we should always **prioritise using the most abundant character** — but with a critical constraint. If the last two characters in our result are the same, we must pick a *different* character next, even if it's not the most abundant. Think of it like a balancing act: we want to "burn through" the character with the highest count as fast as possible (using it twice in a row when allowed), while using less frequent characters as "separators" to break up potential triplets. This greedy strategy works because: 1. By always picking the most frequent valid character, we maximise the length of the result 2. Using a character twice when possible (aa, bb, cc) is optimal — it depletes the larger counts faster 3. When forced to use a less frequent character as a separator, we only use it once to minimise "waste" A **max-heap** naturally gives us the character with the highest remaining count at each step, making this approach efficient. approach: | We solve this using a **Greedy approach with a Max-Heap**: **Step 1: Build the max-heap** - Create a max-heap containing tuples of `(count, character)` for each character with count > 0 - Use negative counts in Python's `heapq` since it's a min-heap by default   **Step 2: Greedily build the string** - While the heap is not empty: - Pop the character with the highest count - Check the last two characters of the result - **If the last two characters are the same as the popped character**: we cannot use it (would create "aaa", "bbb", or "ccc") - Pop the next most frequent character instead - Use it once, then push both back - If no alternative exists, we're done - **Otherwise**: use the most frequent character - Use it twice if count >= 2 and it won't create a triplet - Use it once otherwise - Push it back if count > 0   **Step 3: Return the result** - Return the built string   The greedy choice of always using the most frequent valid character ensures we build the longest possible happy string. common_pitfalls: - title: Always Using the Most Frequent Without Checking description: | A naive greedy approach might always pick the most frequent character without checking if it would create a triplet. For example, with `a=2, b=2, c=1` and result so far `"aa"`, blindly picking 'a' again would create `"aaa"`. You must check the last two characters of the result before deciding which character to append. wrong_approach: "Always append the most frequent character" correct_approach: "Check last two characters first, switch to second-most-frequent if needed" - title: Using Single Characters When Doubles Are Safe description: | When building the string, if the most frequent character doesn't match the last two, we can safely append it **twice** (if count >= 2). Using only one character at a time when two are safe means we don't deplete the larger counts fast enough, potentially leaving characters unused. For example, with `c=7, a=1, b=1`: optimal is "ccaccbcc" (length 8), not "cacbccc" (length 7). wrong_approach: "Always append just one character at a time" correct_approach: "Append two characters when it's safe and count allows" - title: Not Handling the "No Valid Character" Case description: | When the last two characters are the same as the most frequent, and there's no second character available, the string is complete. Failing to handle this edge case can cause infinite loops or index errors. For example, with `a=3, b=0, c=0`, the answer is `"aa"` — we cannot use all three 'a's. wrong_approach: "Assume there's always a valid character to append" correct_approach: "Check if heap is empty after skipping the blocked character" key_takeaways: - "**Greedy with constraints**: Always pick the locally optimal choice (most frequent), but respect the constraint (no triplets)" - "**Max-heap for dynamic priorities**: When the 'best' option changes as you consume resources, a heap keeps priorities efficiently updated" - "**Double usage optimisation**: When allowed, use the most frequent character twice to deplete large counts faster" - "**Pattern recognition**: This problem combines greedy character selection with the 'reorganise string' pattern seen in problems like Task Scheduler" time_complexity: "O((a + b + c) * log 3) = O(n). Each character is pushed and popped from the heap at most once, and heap operations on 3 elements are O(log 3) = O(1)." space_complexity: "O(a + b + c) = O(n). The result string stores up to `a + b + c` characters. The heap uses O(1) space since it contains at most 3 elements." solutions: - approach_name: Greedy with Max-Heap is_optimal: true code: | import heapq def longest_diverse_string(a: int, b: int, c: int) -> str: # Max-heap: use negative counts for max-heap behaviour heap = [] if a > 0: heapq.heappush(heap, (-a, 'a')) if b > 0: heapq.heappush(heap, (-b, 'b')) if c > 0: heapq.heappush(heap, (-c, 'c')) result = [] while heap: # Get the most frequent character count1, char1 = heapq.heappop(heap) # Check if last two chars are the same as char1 if len(result) >= 2 and result[-1] == char1 and result[-2] == char1: # Can't use char1, try the second most frequent if not heap: break # No alternative, we're done count2, char2 = heapq.heappop(heap) result.append(char2) # Use only once as separator count2 += 1 # Decrement (negative, so add 1) if count2 < 0: heapq.heappush(heap, (count2, char2)) # Push char1 back unchanged heapq.heappush(heap, (count1, char1)) else: # Safe to use char1 — use twice if possible if -count1 >= 2: result.append(char1) result.append(char1) count1 += 2 else: result.append(char1) count1 += 1 if count1 < 0: heapq.heappush(heap, (count1, char1)) return ''.join(result) explanation: | **Time Complexity:** O(n) where n = a + b + c — Each character is used at most once, and heap operations on at most 3 elements are O(1). **Space Complexity:** O(n) — The result string can be up to length n. We greedily select the most frequent valid character at each step. When the most frequent would create a triplet, we use the second-most-frequent as a separator. Using characters twice when safe maximises output length. - approach_name: Greedy Without Heap is_optimal: false code: | def longest_diverse_string(a: int, b: int, c: int) -> str: result = [] counts = [a, b, c] chars = ['a', 'b', 'c'] while True: # Find the character with max count that won't create triplet # Sort indices by count descending order = sorted(range(3), key=lambda i: -counts[i]) added = False for i in order: if counts[i] == 0: continue # Check if this char would create a triplet if (len(result) >= 2 and result[-1] == chars[i] and result[-2] == chars[i]): continue # Safe to add this character result.append(chars[i]) counts[i] -= 1 # Try to add a second one if safe if (counts[i] > 0 and (len(result) < 2 or result[-1] != chars[i] or result[-2] != chars[i])): # Check if adding another would still be safe # (won't create triplet with what follows) # We only add two if this char has the max count # to deplete it faster if i == order[0]: result.append(chars[i]) counts[i] -= 1 added = True break if not added: break return ''.join(result) explanation: | **Time Complexity:** O(n) — Each iteration adds 1-2 characters, and sorting 3 elements is O(1). **Space Complexity:** O(n) — The result string can be up to length n. This approach manually tracks counts and sorts to find the most frequent valid character. While functionally equivalent, it's less elegant than the heap solution and slightly harder to generalise to more characters.