Overview

5 What’s up with you, Directed Acyclic Word Graph?

This chapter introduces the problem of storing and searching large word lists efficiently, especially for operations such as checking whether a word exists and finding all words with a given prefix. It compares simple approaches first: hash sets are excellent for exact lookup but poor for prefix search, while sorted lists support both operations reasonably well through binary search but still require extra work to enumerate matches and can consume significant memory when stored as ordinary strings.

The chapter then develops tries, or prefix trees, as a better fit for prefix-based searching. A trie stores shared prefixes only once, allowing word lookup and prefix navigation in time proportional to the length of the input string rather than the size of the word list. However, tries can still waste substantial memory because many nodes, especially leaf nodes and common suffix patterns, are duplicated. This motivates viewing tries as finite state automata, where nodes are states, edges are character transitions, and accepted strings are the stored words.

The final major idea is the Directed Acyclic Word Graph, which improves on a trie by deduplicating suffixes as well as prefixes. The chapter explains an incremental algorithm for building a minimal DAWG from a sorted word list: each new word is added while the previously added word’s unique suffix is optimized by replacing equivalent nodes with canonical ones. This preserves efficient search operations while greatly reducing node count in realistic word lists. The chapter concludes that DAWGs offer an elegant and practical balance of fast lookup, prefix search, and compact storage, especially when combined with low-level byte-array representations for memory-constrained systems.

The trie for a nine-word list, laid out here left to right, and in alphabetical order from top to bottom. Node numbers are not data in the tree; they are just so we can refer to them in the discussion which follows. Double-circle nodes represent ends of words.
The trie for the word list “car”
The trie for the word list “car”, “care”.
A trie with nine words: car, care, cares, cars, fir, fire, firer, firers, firs
Deduplicating all the leaves of the trie produces an equivalent word graph.
We’ve added car, care, cares and cars. The graph is optimized for everything except “cars”. Node 6 is identical to node 5, but node 6 is in “cars” so we have not fixed it yet.
After adding “fir” the graph is optimized for every word except “fir”. State 9 could be replaced with state 5, so this graph is not optimized for the final word.
Do you notice something odd about this graph after adding “fire”?
After adding “firer” and “firers”, the nodes for “firers” are still not optimized.
We look for optimizations moving backwards from the end of the previous word added, and first discover that node 12 is redundant; node 11 should have an edge to node 5 instead.
We’ve now fully optimized the “ers” portion of “firers” and we’re ready to add “firs”.
“firs” is added, and everything is optimized except for node 13, which is redundant to node 4.
This is the smallest possible DAWG that represents those nine words.

Summing Up

  • Storing n words of average length m in a list such that we can generate all the words matching a particular prefix is a common problem. The number of words can be very large.
  • Hash sets are great at determining if a given word is contained in a set, but not at all good at generating completions given a prefix.
  • We can get O(m log n) search performance by using binary search on a sorted list, which is probably acceptable -- but this solution uses a surprisingly large amount of memory if we use off-the-shelf parts.
  • We can get O(m) search performance by using a trie to deduplicate prefixes, but the number of redundant nodes is potentially enormous.
  • By noticing that tries are a special case of finite state automata, we could use any one of many algorithms that minimize FSAs without changing the language they recognize. But starting with the huge, space-inefficient trie in memory and running an expensive, difficult minimization algorithm is not great; what we really want is to build an optimized DAWG one word at a time.
  • Efficiently detecting when two nodes are redundant and can be safely merged is a key sub-problem in graph minimization; if we are careful about not mutating a node after it is in the canonical set of optimized nodes, we can do so very efficiently.
  • We can make DAWGs very small in memory if we need to; this is exactly what game developers did back in the day.

FAQ

What problem motivates the use of tries and DAWGs in this chapter?The motivating problem is storing a large word list so that common operations are fast, especially Contains—checking whether a word is legal—and StartsWith—listing all words beginning with a prefix, as in autocomplete or word-game move generation.
Why is a hash set not a good solution for prefix searching?A HashSet<string> is excellent for Contains, because hashing a word takes O(m) time where m is the word length. However, hash sets deliberately distribute similar-looking words across different buckets, so they provide no efficient way to find all words that begin with a given prefix.
How does a sorted list support prefix search?A sorted list can use binary search to find either the prefix itself or the first word that would come after the prefix. From that position, it scans forward and yields words while they continue to start with the prefix. This gives reasonable performance, but finding the starting point costs O(m log n), and checking each matching word’s prefix costs additional time.
What is a trie?A trie, or prefix tree, is a tree-shaped data structure that deduplicates common prefixes. Each edge is labeled with a character, and each node represents a position between characters. A node is marked as an end-of-word node if the path from the root to that node spells a word in the list.
How are words added to a trie?To add a word, start at the root and process each character in order. If an edge for the character already exists, follow it. Otherwise, create a new edge and node. After the final character, mark the resulting node as an end-of-word node.
What are the time complexities of trie operations?Building a trie from n words of average length m takes O(mn), because every character must be processed. Checking whether a word exists takes O(m), since at most one edge is followed per character. Finding the node for a prefix also takes O(m), after which matching words can be enumerated from that node.
Why might a trie still use a lot of memory?Although a trie deduplicates prefixes, it can contain many duplicate suffix structures. For example, many leaf nodes may be identical end-of-word nodes with no outgoing edges. In the ENABLE word list example, the trie had 387,881 nodes, including 111,507 identical leaves, so the object overhead can be substantial.
What is a DAWG?A DAWG is a Directed Acyclic Word Graph. It represents a word list like a trie, but it deduplicates both common prefixes and common suffixes. Because suffixes may be shared by multiple prefixes, the structure is no longer a tree; it is an acyclic graph.
How is a DAWG related to finite state automata?A trie or DAWG can be viewed as a finite state automaton. Nodes are states, edge labels are inputs, the root is the start state, and end-of-word nodes are accepting states. The word list is the language accepted by the automaton. Minimizing the automaton means reducing it to the fewest states while accepting the same words.
How effective is a DAWG compared with a trie for the ENABLE word list?For the ENABLE benchmark, the trie had 387,881 nodes and 387,880 edges, while the DAWG had only 54,167 nodes and 122,975 edges. That means the DAWG used only about 14% as many nodes as the trie, mainly because it eliminated redundant leaves and shared common English suffixes such as -s, -ed, and -ing.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Fabulous Adventures in Data Structures and Algorithms ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Fabulous Adventures in Data Structures and Algorithms ebook for free