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.
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:
- Quickly get the character with the most remaining occurrences
- When needed, temporarily set aside the most frequent character and pick the second most frequent one
- 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:
-
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'])
-
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
-
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 EvaluatorExample 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 mosta + 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.
Which of the following is a min heap?
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!