Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Trees, Heaps & Tries  ›  Capstone Project

Capstone — Discovery Feed

35 minA tree, a trie and a heap working as one pipeline
You're building a piece ofTunebox — discovery
This piece — discovery_feed(): Builds a personal feed from genres, charts and search.
Scenario A listener who loves Rock opens Tunebox's discovery tab and types "ne". The feed shows the best Rock, Punk and Metal tracks starting with "ne", most-played first.
Your task
Build discovery_feed(genres, tracks, favorite, prefix, k). Use the genre tree (a level-order list) to find favorite and every genre beneath it; a trie to find those genres' tracks whose titles start with prefix; and a heap to return the k most-played titles, ties alphabetical. tracks are [title, genre, plays] triples with unique titles. Example: with genres = ["Music", "Rock", "Electronic", "Punk", "Metal", None, "House"] and tracks = [["nebula", "Metal", 240], ["needle drop", "Punk", 95], ["glasshouse", "Rock", 500], ["neon rain", "House", 120]], discovery_feed(genres, tracks, "Rock", "ne", 2) → ["nebula", "needle drop"].

Capstone — Discovery Feed

The discovery tab is where Tunebox's pieces meet. A listener who loves Rock types "ne", and the feed should show the best few Rock tracks — Punk and Metal included — whose titles start with "ne", most-played first. That's three questions, and this section has already built a structure for each: the genre tree says which genres count, a trie says which titles match, and a heap picks the top k.

The spec, pinned down

discovery_feed(genres, tracks, favorite, prefix, k) takes:

  • genres — the genre tree as a level-order list, None for a missing child
  • tracks[title, genre, plays] triples; titles are unique and lowercase
  • favorite — a genre; the feed draws from it and every genre beneath it
  • prefix — what's been typed so far; "" matches everything
  • k — how many titles to return

It returns up to k titles, most plays first, ties broken alphabetically. A favorite that isn't in the tree gives [], and a track whose genre isn't in the tree is never picked.

Pinning ties down isn't pedantry. Two listeners with the same taste must see the same feed, and a test can only check an answer that has exactly one right value.

Stage 1 — the genre tree: which genres count?

Find the favorite's node, then gather its whole subtree. Both are small recursions from the binary trees lesson:

python

find(...) or find(...) works because a node is truthy and None isn't, so the right subtree is only searched when the left one came back empty.

What does this print?
class Node:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right

def names(node):
    if node is None:
        return set()
    return {node.val} | names(node.left) | names(node.right)

electronic = Node("Electronic", Node("Techno", None, Node("Acid")), Node("House"))
print(len(names(electronic.left)))

Stage 2 — the trie: which titles match?

Insert the titles of the tracks that survived stage 1, walk down the prefix, and collect everything below. Order doesn't matter yet — stage 3 does the ranking — so a plain stack will do:

python

❓ Cross-question — "For one query, isn't title.startswith(prefix) simpler?" Honestly, yes: one filter over a few thousand tracks is a single line, and fast enough. The trie pays off when it's built once and queried on every keystroke, because each query touches only the typed letters and the matching subtree. The ⚡ section below keeps the index alive between keystrokes.

Stage 3 — the heap: which k are best?

Rank the matches by (-plays, title). Negating puts the most plays first in Python's min-heap, and the title settles ties. Getting that key backwards is the classic bug:

broken — fix it

Ties should be broken alphabetically, but this puts 'never again' before 'needle drop'.

heapq.nsmallest(k, tracks, key=lambda t: (-t[1], t[0])), or heapify followed by k pops, gets the order right. You can negate a number but not a string, which is why the number flips and the title stays as it is.

Filter first, then rank

The stages run so each one hands the next fewer candidates. Step through a miniature pipeline and watch them shrink before the heap ever sees them:

step through it
1import heapq
2 
3allowed = {"Rock", "Punk", "Metal"}
4tracks = [["nebula", "Metal", 240], ["neon rain", "House", 120],
5 ["needle drop", "Punk", 95], ["glasshouse", "Rock", 500]]
6plays = {}
7for title, genre, count in tracks:
8 if genre in allowed and title.startswith("ne"):
9 plays[title] = count
10heap = [(-count, title) for title, count in plays.items()]
11heapq.heapify(heap)
12top = [heapq.heappop(heap)[1] for _ in range(min(2, len(heap)))]
13print(top)

(startswith stands in for the trie to keep the trace short.) Had ranking come first, the heap would have ordered every track in Tunebox only to throw most of them away.

What does this print?
import heapq

plays = {"nebula": 240, "needle drop": 95, "never again": 95}
heap = [(-count, title) for title, count in plays.items()]
heapq.heapify(heap)
print([heapq.heappop(heap)[1] for _ in range(min(5, len(heap)))])
Stage Structure Cost
Build the genre tree queue O(G)
Find the favorite and its subtree recursion O(G)
Build a trie of the allowed titles trie O(C), C = their characters
Walk the prefix and collect matches trie + stack O(p + size of that subtree)
Pick the top k of m matches heap O(m + k log m)

The whole feed is roughly linear in its input. Once the indexes exist, a keystroke only pays for the last two rows.

Coming from Java/JS: this is a stream pipeline — filter, filter, sorted(comparator).limit(k) — with one difference that matters: a heap never sorts the survivors it doesn't return.


Idioms & real-world patterns

Build once, query per keystroke

In a real app the indexes outlive a single call. A class builds them once and answers every keystroke; the graded function is the one-shot version:

python

⚡ Advanced — the best few on every trie node

If each trie node stores its best few titles, updated as titles are inserted, a keystroke needs no heap at all: walk one step and read the list. Large autocomplete systems do exactly this, trading memory and slower inserts for instant reads.

⚡ Advanced — charts that change while you read them

Plays keep arriving. Rather than rebuild the heap, push the updated (-plays, title) entry and discard stale ones as they surface — the lazy-deletion trick from the heaps lesson.


🎯 Your turn

Write discovery_feed(genres, tracks, favorite, prefix, k). Use the genre tree to find favorite and every genre beneath it, a trie to find their titles starting with prefix, and a heap to return the k most-played, ties alphabetical. With genres = ["Music", "Rock", "Electronic", "Punk", "Metal", None, "House"] and tracks = [["nebula", "Metal", 240], ["needle drop", "Punk", 95], ["glasshouse", "Rock", 500], ["neon rain", "House", 120]]:

  • discovery_feed(genres, tracks, "Rock", "ne", 2)["nebula", "needle drop"]
  • discovery_feed(genres, tracks, "Jazz", "", 3)[]

Hint — three stages, in order: names(find(build(genres), favorite)), then a trie of the allowed titles walked down prefix, then (-plays, title) pairs on a heap.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = the discovery feed is live, and this part of Tunebox is complete. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upEverything under a favorite genre

Write subgenres(genres, favorite) returning favorite and every genre beneath it, in pre-order (a genre before its sub-genres, left before right). The tree is a level-order list with None for a missing child. If favorite isn't in the tree, return []. build(genres) is provided.

treepreorder
DrillRank the candidates

Write top_tracks(tracks, k). Each track is [title, genre, plays] and titles are unique. Use heapq to return the titles of the k most-played tracks, most plays first, ties broken alphabetically.

top-kheapq
BuildSearch the genre tree by name

Write genre_search(genres, typed). The tree is a level-order list with None for a missing child. Return the full path — like "Music > Rock > Punk" — of every genre whose name starts with typed, sorted alphabetically. Index the genre names in a trie as you walk the tree. build(genres) is provided.

trietraversal
BossThe top hit on every keystroke

Write keystroke_hits(tracks, typed). Each track is [title, plays], titles unique. As a listener types typed one character at a time, Tunebox shows the single most-played title starting with the text typed so far (ties alphabetical), or None if nothing matches. Return one entry per character. Each keystroke must be O(1) work after the index is built — no scanning, no heap per keystroke.

trieautocomplete
CapstoneThe hottest genres, subgenres included

Write hottest_genres(genres, tracks, k). A genre's heat is the total plays of tracks tagged with it plus the heat of every genre beneath it. Tracks are [title, genre, plays]; ignore any whose genre isn't in the tree. Return the k genres with the most heat, highest first, ties alphabetical — genres with no plays count as 0.

postordertop-krecursion
discovery_feed.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
discovery_feed(genres, tracks, favorite, prefix, k) → listUp to k titles from the favorite genre's subtree that start with prefix, most-played first.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.
Capstone — Discovery Feed — Pebells