Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7.10 Longest Happy String

Source: src/main/kotlin/heap/LongestHappyString.kt Pattern: max-heap with a 2-repeat cap · Core page

The Problem

Given a, b, c counts of 'a','b','c', build the longest possible string with no three identical characters in a row.

  • Constraints: $0 \le a, b, c \le 100$.

Examples

Input:  a = 1, b = 1, c = 7   -> Output: "ccaccbcc"   (length 8, no "ccc")
Input:  a = 7, b = 1, c = 0   -> Output: "aabaa"      (length 5, the two b's separate)

Intuition — always take the most frequent letter, but never three in a row

The 11.6/11.10 max-heap machine, with a twist: a letter may appear twice consecutively (only three is banned). So the greedy is:

heap of (letter, count), max by count
lastChar, lastCount (consecutive run of the last char)
while heap not empty:
    (char, count) = poll
    if lastChar == char && lastCount == 2:
        if heap empty: break                    # can't place it: done
        (altChar, altCount) = poll              # take the second-most frequent instead
        append altChar; push altChar back with count-1
        push (char, count) back                 # the blocked char returns
        lastChar = altChar; lastCount = 1
    else:
        append char; push back with count-1
        if lastChar == char: lastCount++ else { lastChar = char; lastCount = 1 }

Why the “second-most frequent” fallback? When the top letter is already at its 2-run cap, placing it would create “aaa”. The next-best letter breaks the run — and the blocked letter goes back into the heap for the next round. This is the 11.10 cooldown logic with cooldown = 1-consecutive instead of k-apart.

Why does the max-heap keep it optimal? The exchange argument (11.0): the most frequent remaining letter should be placed whenever legal — deferring it only makes the tail harder. The “place the second-best” detour is the single exception, forced by the 2-cap.

Approach 1 — Greedy frequency math (O(n) closed form)

The counts-based construction (maxFreq vs the rest) — correct but fiddly; the heap below is the general engine.

Approach 2 — Max-heap with the 2-cap fallback (the repo’s version, optimal)

import java.util.*

class LongestHappyString {
    /**
     * @param a count of 'a'
     * @param b count of 'b'
     * @param c count of 'c'
     * @return  the longest happy string
     */
    fun longestDiverseString(a: Int, b: Int, c: Int): String {
        val pq = PriorityQueue<Pair<Char, Int>> { p1, p2 -> p2.second - p1.second }

        if (a > 0) pq.offer('a' to a)
        if (b > 0) pq.offer('b' to b)
        if (c > 0) pq.offer('c' to c)

        return buildString {
            var lastChar = ' '
            var lastCount = 0

            while (pq.isNotEmpty()) {
                val (char, count) = pq.poll()

                // Two consecutive already: must use the second-highest count character
                if (lastChar == char && lastCount == 2) {
                    if (pq.isEmpty()) break

                    val (altChar, altCount) = pq.poll()
                    append(altChar)
                    lastChar = altChar
                    lastCount = 1

                    if (altCount > 1) pq.offer(altChar to altCount - 1)
                    pq.offer(char to count)          // the blocked char returns
                } else {
                    append(char)
                    pq.offer(char to count - 1)

                    if (lastChar == char) lastCount++
                    else { lastChar = char; lastCount = 1 }
                }
            }
        }.toString()
    }
}
import java.util.*;

public class LongestHappyString {
    /**
     * @param a count of 'a'
     * @param b count of 'b'
     * @param c count of 'c'
     * @return  the longest happy string
     */
    public String longestDiverseString(int a, int b, int c) {
        PriorityQueue<int[]> pq = new PriorityQueue<>((x, y) -> y[1] - x[1]);   // char, count
        if (a > 0) pq.offer(new int[]{'a', a});
        if (b > 0) pq.offer(new int[]{'b', b});
        if (c > 0) pq.offer(new int[]{'c', c});

        StringBuilder sb = new StringBuilder();
        char last = ' ';
        int run = 0;

        while (!pq.isEmpty()) {
            int[] top = pq.poll();

            if (last == (char) top[0] && run == 2) {
                if (pq.isEmpty()) break;
                int[] alt = pq.poll();                    // second-most frequent
                sb.append((char) alt[0]);
                last = (char) alt[0];
                run = 1;
                if (alt[1] > 1) pq.offer(new int[]{alt[0], alt[1] - 1});
                pq.offer(top);                            // the blocked char returns
            } else {
                sb.append((char) top[0]);
                pq.offer(new int[]{top[0], top[1] - 1});
                if (last == (char) top[0]) run++;
                else { last = (char) top[0]; run = 1; }
            }
        }
        return sb.toString();
    }
}
#include <queue>
#include <string>

class LongestHappyString {
public:
    /**
     * @param a count of 'a'
     * @param b count of 'b'
     * @param c count of 'c'
     * @return  the longest happy string
     */
    std::string longestDiverseString(int a, int b, int c) {
        auto cmp = [](auto& x, auto& y) { return x.second < y.second; };
        std::priority_queue<std::pair<char, int>,
            std::vector<std::pair<char, int>>, decltype(cmp)> pq(cmp);

        if (a) pq.push({'a', a});
        if (b) pq.push({'b', b});
        if (c) pq.push({'c', c});

        std::string result;
        char last = ' ';
        int run = 0;

        while (!pq.empty()) {
            auto [ch, cnt] = pq.top(); pq.pop();

            if (last == ch && run == 2) {
                if (pq.empty()) break;
                auto [alt, altCnt] = pq.top(); pq.pop();   // second-most frequent
                result += alt;
                last = alt;
                run = 1;
                if (altCnt > 1) pq.push({alt, altCnt - 1});
                pq.push({ch, cnt});                        // the blocked char returns
            } else {
                result += ch;
                pq.push({ch, cnt - 1});
                if (last == ch) run++;
                else { last = ch; run = 1; }
            }
        }
        return result;
    }
};
import heapq

def longest_diverse_string(a: int, b: int, c: int) -> str:
    """
    @param a: count of 'a'
    @param b: count of 'b'
    @param c: count of 'c'
    @return:  the longest happy string
    """
    heap = []
    for ch, cnt in (("a", a), ("b", b), ("c", c)):
        if cnt:
            heapq.heappush(heap, (-cnt, ch))

    result = []
    last, run = "", 0

    while heap:
        cnt, ch = heapq.heappop(heap)
        if last == ch and run == 2:
            if not heap:
                break
            alt_cnt, alt = heapq.heappop(heap)   # second-most frequent
            result.append(alt)
            last, run = alt, 1
            if alt_cnt + 1 < 0:
                heapq.heappush(heap, (alt_cnt + 1, alt))
            heapq.heappush(heap, (cnt, ch))      # the blocked char returns
        else:
            result.append(ch)
            if cnt + 1 < 0:
                heapq.heappush(heap, (cnt + 1, ch))
            if last == ch:
                run += 1
            else:
                last, run = ch, 1

    return "".join(result)
#![allow(unused)]
fn main() {
use std::cmp::Reverse;
use std::collections::BinaryHeap;

impl Solution {
    /// @param a count of 'a'
    /// @param b count of 'b'
    /// @param c count of 'c'
    /// @return  the longest happy string
    pub fn longest_diverse_string(a: i32, b: i32, c: i32) -> String {
        let mut heap: BinaryHeap<(i32, char)> = BinaryHeap::new();
        for (cnt, ch) in [(a, 'a'), (b, 'b'), (c, 'c')] {
            if cnt > 0 { heap.push((cnt, ch)); }
        }

        let mut result = String::new();
        let (mut last, mut run) = (' ', 0);

        while let Some((cnt, ch)) = heap.pop() {
            if last == ch && run == 2 {
                if heap.is_empty() { break; }
                let (alt_cnt, alt) = heap.pop().unwrap();   // second-most frequent
                result.push(alt);
                last = alt;
                run = 1;
                if alt_cnt > 1 { heap.push((alt_cnt - 1, alt)); }
                heap.push((cnt, ch));                       // the blocked char returns
            } else {
                result.push(ch);
                if cnt > 1 { heap.push((cnt - 1, ch)); }
                if last == ch { run += 1; }
                else { last = ch; run = 1; }
            }
        }
        result
    }
}
}

Dry run

Input: a = 1, b = 1, c = 7.

heap: c(7), a(1), b(1).  last=' ', run=0
c(7): not capped -> append 'c'.  push c(6).  last='c', run=1
c(6): same char, run 1 < 2 -> append 'c'.  push c(5).  last='c', run=2
c(5): same char, run == 2 -> CAP.  take alt a(1): append 'a'.  push c(5) back.  last='a', run=1
c(5): not capped -> 'c'.  push c(4).  run on 'c' resets to 1
c(4): 'c'.  push c(3).  run=2
c(3): CAP -> alt b(1): append 'b'.  push c(3) back.
c(3): 'c'.  c(2): 'c'.  c(1): CAP -> heap empty? a,b used... heap has c(1): alt needed, heap empty -> break.

result: "ccaccbcc" ✓   (8 chars, no "ccc")

The cap dance in action: after “cc”, the third c is blocked and the heap’s second-best (a, then b) breaks the run. The blocked c returns immediately and resumes — producing the maximal “ccac cbcc” shape. The run counter only counts consecutive same-char placements; any different char resets it.

Complexity

Time. Each placement O(log 3):

$$ T(n) = O(n \log 3) = O(n) $$

Space. The heap + result:

$$ S(n) = O(n) $$

Variants & follow-ups

  • Reorganize String (11.10) — the same heap + cap machine with cooldown = 1 (no repeats at all).
  • Task Scheduler (11.6) — the cooldown-window family ancestor.
  • Interview follow-up: “Why is the ‘second-most frequent’ fallback optimal?” Placing the blocked top letter would create the forbidden triple. Any legal placement must use a different letter; the most frequent different letter is the best choice by the exchange argument (it leaves the counts most balanced). If none exists, the string is maximal.