Recommended resources
The freely available resources this lesson will direct you to.
Hello! In our last lesson, we saw how a Trie's ability to efficiently check for prefixes could power an autocomplete feature. The core idea was to navigate to a prefix node and then explore the subtree below it. Today, we'll apply that same fundamental principle—leveraging prefixes—to solve a much more complex problem and achieve a significant performance optimization.
This lesson addresses how a Trie can optimize a backtracking search for words on a 2D grid, a classic problem often called "Word Search II." You will learn how the Trie's structure allows us to transform a slow, brute-force approach into an elegant and efficient one by intelligently pruning the search space. This pattern is a powerful combination of two topics you're exploring: Tries and Backtracking.
The Problem: Word Search II
Imagine a standard word search puzzle. You are given a 2D grid of characters and a dictionary of words. Your goal is to find all the words from the dictionary that can be formed by traversing adjacent cells (horizontally or vertically) in the grid. A single cell cannot be used more than once within the same word.

You can read a more formal description of the problem setup in the following resource.
212. Word Search II - In-Depth Explanation
This article from AlgoMonster introduces the Word Search II problem and its constraints.
Please read the section titled "Problem Description". This will solidify your understanding of the rules and the goal.
The Naive Approach and its Bottleneck
Given your experience with LeetCode and backtracking, your first instinct might be to adapt the solution for the simpler "Word Search I" problem. A straightforward approach would be to iterate through each word in the dictionary and, for each one, perform a full backtracking search on the grid to see if it exists.
This would look something like:
results = []
for word in dictionary:
if exists(board, word): # exists() is a full backtracking search
results.append(word)
This works, but it's highly inefficient. Why? Think about a dictionary containing the words "oat", "oath", and "oathkeeper". The naive algorithm would traverse the grid searching for "oat". Then, it would start all over again, traversing many of the same o -> a -> t paths, to search for "oath". This massive amount of redundant work is the critical bottleneck.
The following video segment explains this inefficiency clearly.
Word Search II - Backtracking Trie - Leetcode 212 - Python
In this NeetCode video, the presenter breaks down the Word Search II problem and the issue with a brute-force approach.
Watch from the beginning until this point, where he explains why running a separate DFS for each word is inefficient.
The Key Insight: Pruning with a Trie
The inefficiency stems from checking words one by one. The optimization, therefore, is to search for all words simultaneously in a single pass over the grid. How can we do this? The shared prefixes are the key.
Instead of asking "Does 'oath' exist?", we can ask a more powerful question during our traversal: "Is the current path I'm exploring (e.g., 'o-a-t') a valid prefix for any word in my dictionary?"
This is precisely the problem a Trie is designed to solve. By pre-processing our dictionary into a Trie, we can use it as a guide during our backtracking search. As we explore the grid, we also traverse the Trie. If we try to move to a character on the grid that does not correspond to a valid child in our current Trie node, we know that no word in our dictionary can be formed by continuing down that path. We can immediately prune that entire branch of the search and backtrack.
This video provides an excellent visualization of this core concept.
Word Search II - Backtracking Trie - Leetcode 212 - Python
The same NeetCode video goes on to explain and visualize how a Trie solves the problem of redundant work.
Watch the segments from here, where the Trie data structure is introduced as the solution, and then from this segment, which walks through a grid example showing how the Trie prunes search paths.
The Combined Algorithm: Trie-Guided Backtracking
By combining backtracking with a Trie, we arrive at an elegant and efficient algorithm. Let's trace the logic of how these two concepts work together.
- Build the Trie: First, we insert every word from our dictionary into a Trie.
- Iterate and DFS: We iterate through every cell
(r, c)on the board. Each cell is a potential starting point for a word. From each cell, we launch a recursive backtracking (DFS) function. - The DFS State: Our
dfsfunction needs to keep track of its current position on the board,(r, c), and its current position in the Trie, let's call itnode. - Pruning in Action: Inside the
dfs(r, c, node):- The base cases for stopping are: going out of bounds, or visiting a cell we've already used in the current path.
- The key step: We look at the character on the board,
char = board[r][c]. We check ifcharis a child of the current Trienode. - If
node.childrendoes not containchar, we prune. We stop this path andreturnimmediately. There's no need to explore further. - If it does exist, we advance our Trie pointer to that child node:
new_node = node.children[char]. - We then check if this
new_nodemarks the end of a word (e.g.,new_node.isEndOfWord == True). If it does, we've found a valid word! We add it to our results. - Finally, we recurse on all valid neighbors, passing along the
new_nodeto guide the subsequent search.
The following video provides a fantastic step-by-step visualization of this process, showing the grid traversal and the Trie state side-by-side.
Word Search 2 | Backtracking and Trie Data Structure | Leetcode 212
This video by Nideesh Terapalli gives a detailed walkthrough of the combined backtracking and Trie search.
Watch the segment from "Backtracking part of the question". Pay close attention to how the board cells are marked as visited (#) and how the "control" is passed to different nodes in the Trie as the path on the grid gets longer. This visualizes the pruning process perfectly.
From Concept to Implementation
Now that you have the core intuition, let's look at how this is structured in code. The following article provides a clear, step-by-step explanation that mirrors the logic we just discussed.
212. Word Search II - In-Depth Explanation
This AlgoMonster article provides a detailed breakdown of the intuition and implementation.
First, read the Intuition section to reinforce the "why" behind using a Trie. Next, study the Solution Approach, which breaks down the implementation into building the Trie and the DFS function. Finally, walk through the Example Walkthrough. This provides a concrete example of how the algorithm processes a small board and word list, highlighting how and when the search is pruned.
A crucial detail for a robust implementation is handling the case where a word is found. If we find "oat", we should add it to our results. What if we later find "oath"? We should add that too. But what if the word "oat" could be formed two different ways on the board? We should only add it to our results once. A simple way to handle this is to add results to a set, which automatically handles duplicates. An even better optimization is to modify the Trie node itself once a word is found (e.g., by setting node.isEndOfWord = False). This prevents the same word from ever being added again.
This and other common implementation pitfalls are discussed in the final section of the AlgoMonster article.
212. Word Search II - In-Depth Explanation
This section covers common mistakes that can occur when implementing this algorithm.
Read the section "Common Pitfalls", focusing on the first two points: "Not Handling Duplicate Words in Results" and "Incorrect Backtracking". These are essential for writing correct code.
Conclusion
In this lesson, you've seen how a data structure—the Trie—can fundamentally change the approach to a complex backtracking problem. Instead of a series of inefficient, independent searches, we can perform one intelligent, guided search.
Key Takeaways:
- The naive solution for Word Search II (running Word Search I for every word) is inefficient due to redundant exploration of common prefixes.
- A Trie allows us to search for all words simultaneously by organizing them based on shared prefixes.
- The core optimization is pruning: during the backtracking DFS on the grid, we use the Trie to check if the current path corresponds to a valid prefix. If not, we immediately stop exploring that path.
- The combined algorithm involves a DFS on the grid where the state includes not only the grid position but also the current node in the Trie, which acts as a guide for the search.
You now have a solid conceptual foundation for this powerful pattern. In our next lesson, we will put this knowledge into practice by implementing a complete backtracking function that traverses the grid, using a Trie to prune invalid search paths and solve the Word Search II problem efficiently.