latency.lab
Algorithms · Topic 08

Tries

Level: foundationTool: prefix matching, and trie nodes as more than a flagTime: ~65 min

By the end you can

A trie answers one specific question efficiently that a hash set can't: not just "have I seen this exact string," but "have I seen anything that starts this way." Every problem in this topic is some variation of exploiting that — prefix lookup, prefix-based aggregation, or walking multiple candidate strings at once because they share structure.

What a trie actually is

A trie is a tree where each edge represents one character, and each root-to-node path represents the prefix spelled out by the characters along the way. A node marks "a complete word ends here" with a flag — critically, a node being reachable (someone walked through it) is not the same as a node being an endpoint (some inserted word actually ends there). Confusing "this prefix exists" with "this exact word was inserted" is the single most common trie bug, and Implement Trie's search vs. starts_with distinction exists specifically to force you to keep them separate.

The habit to build

Before coding, ask: "does this problem need me to find complete matches, or does it need me to reason about shared prefixes?" If it's about prefixes — autocomplete, the shortest matching root, aggregating values across everything that starts a certain way — a trie is very likely the right tool. If it's just "have I seen this exact thing," a hash set is simpler and just as fast.

root A "CA": reachable, not a word R T
Trie holding "CAR" and "CAT". Walking C→A reaches a real node (starts_with("ca") is true), but that node was never marked as a complete word (search("ca") is false) — only the teal end-of-word nodes at R and T represent actual inserted words.

A trie node can hold more than just a flag

Most first examples only ever store a boolean "is this a complete word." Map Sum Pairs breaks that assumption immediately: every node instead accumulates a running sum of every value inserted through it, so a prefix query is a single O(L) walk instead of enumerating and summing every matching key. Word Search II's trie nodes store the actual word string at completion points, so a match reports itself directly without needing to reconstruct the path. The lesson: a trie node is just a bucket you can attach whatever aggregate the problem needs to — it doesn't have to be limited to a flag.

The alphabet doesn't have to be letters

Maximum XOR of Two Numbers in an Array is the clearest proof that "trie" doesn't mean "strings." Insert each number's binary representation, one bit at a time, into a trie with exactly 2 children per node instead of 26 — the exact same structure, just a 2-symbol alphabet instead of a 26-symbol one. Once you see this, you can build a trie over anything with a fixed, ordered set of "next symbol" choices — it's a general prefix-matching structure, strings are just the most common alphabet.

bit-trie-vs-string-trie.cpp
// A string trie: 26 possible next characters.
struct StringTrieNode { StringTrieNode* children[26] = {}; bool is_end = false; };

// A bit trie: exactly the same idea, only 2 possible next bits.
struct BitTrieNode { BitTrieNode* children[2] = {}; };
// Walking bit-by-bit (most significant first) and greedily choosing the
// OPPOSITE bit at each level is what maximizes XOR -- the trie just needs
// to answer "does a number with this bit prefix exist," same as any
// string trie answers "does a word with this letter prefix exist."

Multiple candidate strings, walked together

Word Search II is the clearest case for "why build one shared trie instead of searching separately." A naive per-word DFS repeats the exact same board exploration once per word, even for words sharing a prefix. Building one trie from every word and walking the board and trie together means shared prefixes are only ever explored once — the trie itself is what lets many separate searches collapse into a single pass.

Common mistakes

  • Treating "reachable" as "a complete word" — a node existing because some longer word passes through it doesn't mean that exact prefix was itself inserted as a word (Implement Trie's whole point).
  • Extracting and hashing substrings instead of walking the trie incrementally — Index Pairs of a String and Concatenated Words both specifically avoid building a fresh substring at every candidate split point, walking trie nodes directly instead — the difference between O(n · maxWordLength) and O(n² · avgWordLength).
  • Forgetting a node can hold more than a boolean — Map Sum Pairs needs a running sum at every node, not just at the end; missing this turns an O(L) prefix query back into an O(matching keys) enumeration.
  • Not handling the overwrite case in aggregating structures — Map Sum Pairs must propagate a *delta* (new minus old value) up the whole path on a re-insert, not just add the new value, or every ancestor's sum silently drifts wrong.

Takeaways

  • A trie answers "does anything start with this prefix" in O(L) time — the specific advantage a hash set doesn't give you.
  • A node's "reachable" and "is a complete stored word" are two different facts — keep them as two separate pieces of state, never assume one implies the other.
  • A trie node can hold any aggregate the problem needs (a sum, a short list of candidates, the word itself) — it isn't limited to a single boolean flag.
  • The alphabet is arbitrary — a fixed 2-branch "bit trie" is exactly the same structure as a 26-branch string trie, just over a different symbol set.

Try it: the foundation

Implement Trie (Prefix Tree) — get the reachable-vs-complete-word distinction automatic here, since every later problem in this topic depends on it.

Live

Loading starter code…

Try it: a node holding more than a flag

Map Sum Pairs is the clearest demonstration that a trie node can carry a running aggregate, not just a boolean — the delta-propagation trick here is worth sitting with.

Live

Loading starter code…

Try it: a trie over bits, not letters

Maximum XOR of Two Numbers in an Array is the walkthrough above, in full — the exact same trie idea, just with a 2-symbol alphabet instead of 26.

Live

Loading starter code…

Keep going

That's 3 of the 12 problems in this topic. Word Search II is worth doing once these three feel automatic — it's the clearest case for why walking one shared trie beats searching per word. See the full Tries set →

Checkpoint · Tries

5 questions · pass at 70%

Q01
Implement Trie distinguishes search(word) (exact match) from starts_with(prefix) (any word begins this way). Why can't a single check answer both?
WhyIf "app" is inserted but not "ap", walking to the node for "ap" succeeds (it's reachable, satisfying starts_with) but that node's end-of-word flag is false (satisfying search's stricter requirement of false). Conflating the two is the classic trie bug this problem is built to catch.
Q02
Maximum XOR of Two Numbers in an Array builds a trie with only 2 children per node instead of 26. What does this reveal about what a trie actually is?
WhyThe same principle (branch on the next symbol, one level per symbol in the sequence) applies whether the alphabet is 26 letters or 2 bits. Recognizing that the underlying structure doesn't care what the symbols represent is the actual insight here.
Q03
Map Sum Pairs stores a running SUM at every trie node instead of just a boolean end-of-word flag. Why does this matter for the sum(prefix) query's complexity?
WhyA trie node isn't limited to a single boolean -- here it holds a running aggregate. That's what turns sum(prefix) from "enumerate and add up every matching key" into a single O(L) walk that reads off a value already computed during insertion.
Q04
Why does overwriting an existing key in Map Sum Pairs need to propagate a DELTA (new value minus old value) up the trie path, rather than just adding the new value at each node?
WhyEvery node on the key's path already has the old value baked into its running sum from the first insertion. Adding the full new value again (rather than just the difference) would count the key's contribution twice instead of correctly replacing the old contribution with the new one.
Q05
Word Search II builds ONE shared trie from all the words and does a single DFS per starting cell, rather than running Word Search I's approach once per word. What specifically does this save?
WhyA per-word DFS re-explores the same board paths from scratch for every word, including whatever prefix it shares with other words in the dictionary. Walking one shared trie alongside the board means that shared exploration happens exactly once, no matter how many words branch off from the same prefix.

Finished Tries?

Pass the quiz to complete it automatically.