← All Problems

37. Top-K Frequent Intents

General Pattern Medium Heap — Top-K Frequent
Grounding: Note: general algorithmic pattern relevant to conversational-AI/support-ops engineering; not a confirmed detail of Fin's specific implementation.

Problem

A reporting job scans a stream of intent labels, one per handled conversation (e.g. "billing", "refund", "account"), and needs the k most common intents to surface on a dashboard. Given the full list of intent labels and k, return the k intents that occur most often, most frequent first, without fully sorting every distinct intent by frequency.

Ties in frequency are broken alphabetically ascending. The point of the problem is to avoid an O(m log m) sort over all m distinct intents when you only need the top k of them.

Source: src/37_top_k_frequent_intents.py

def top_k_frequent(intents: list[str], k: int) -> list[str]:

>>> top_k_frequent(["billing","billing","refund","account","refund","billing"], k=2)
['billing', 'refund']
>>> top_k_frequent(["b","a","a","b","c"], k=2)
['a', 'b']

Step-by-Step Approach

  1. Count occurrences of every intent label in a single O(n) pass, using a hash map (e.g. collections.Counter).
  2. Recognize that once you have counts, the remaining question is "select the top k by count" over the m distinct keys — a classic top-k selection problem, not a full sort.
  3. Use a heap-based selection (heapq.nlargest, or an equivalent manual min-heap of size k) over the distinct intents rather than sorting all of them.
  4. Define a sort key that puts higher frequency first, and among equal frequencies, breaks ties alphabetically ascending — for example (count, tuple(-ord(ch) for ch in word)), so that within a tie, "a" and "b" order lexicographically rather than by insertion order.
  5. Handle k = 0 (return an empty list) and k larger than the number of distinct intents (return every distinct intent, still ordered by frequency then alphabetically) as edge cases.
  6. Return the selected intents as a list, most frequent first.

The key insight is that counting is unavoidable at O(n), but selecting the top k of m distinct items only needs O(m log k) via a heap, which beats an O(m log m) full sort whenever k is much smaller than the number of distinct items.

Reference solution

import heapq
from collections import Counter


def top_k_frequent(intents: list[str], k: int) -> list[str]:
    # count once O(n), then heapq.nlargest over the m distinct intents is O(m log k);
    # key negates each char's ord so alphabetically-earlier words win frequency ties
    # distinct intent -> count, one O(n) pass
    counts = Counter(intents)
    # tuple key: count first, then negated ord() to break ties alphabetically
    return heapq.nlargest(k, counts.keys(), key=lambda w: (counts[w], tuple(-ord(ch) for ch in w)))

Key Functions & Tricks

  • collections.Counter(intents) — counts each distinct item in one O(n) pass.
  • heapq.nlargest(k, iterable, key=None) — top-k via bounded heap, O(m log k) not O(m log m).
  • counts.keys() — distinct intents, the candidate pool passed to nlargest.
  • key=lambda w: (counts[w], ...) — tuple key compares lexicographically, count first then tie-break.
  • ord(ch) — character's Unicode code point, used to build a comparable tie-break value.
  • tuple(-ord(ch) for ch in w) — negated code points turn "alphabetically first" into "numerically largest".

How to Recognize This Pattern

Signals: the phrase "top k" or "k most frequent" paired with a request to avoid sorting everything, or an explicit hint that the number of distinct items can be large while k stays small. Any "frequency count, then select the top few" shape is a heap problem: count with a hash map, then use a bounded-size heap (or a language built-in like heapq.nlargest) to pick the top k.

Common variations: "top k frequent elements" over arbitrary hashable items instead of strings; "k closest points to origin" (min-heap by distance instead of by count); or streaming variants where you maintain a running top-k heap of size k as new items arrive, rather than counting everything up front.

Common pitfall: forgetting the tie-breaking rule. If two intents have equal frequency, an interviewer-stated tie-break (like "alphabetically ascending") must be baked into the sort/heap key itself, not left to whatever order the hash map happens to iterate in — Python dict iteration order is insertion order, not alphabetical, so it's easy to get an accidentally-correct-looking answer on small examples that breaks on a real test case.