class TrieNode:
def __init__(self):
self.children = {}
self.word = None
class Solution:
def findWords(self, board, words):
root = TrieNode()
for word in words:
node = root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.word = word
rows, cols = len(board), len(board[0])
res = []
def dfs(r, c, node):
char = board[r][c]
if char not in node.children:
return
next_node = node.children[char]
if next_node.word:
res.append(next_node.word)
next_node.word = None # Avoid duplicates
board[r][c] = "#"
for dr, dc in [(-1,0), (1,0), (0,-1), (0,1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != "#":
dfs(nr, nc, next_node)
board[r][c] = char
for r in range(rows):
for c in range(cols):
dfs(r, c, root)
return res
class TrieNode {
public:
TrieNode* children[26] = {};
string word = "";
};
class Solution {
public:
void insert(TrieNode* root, const string& word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->children[idx]) node->children[idx] = new TrieNode();
node = node->children[idx];
}
node->word = word;
}
void dfs(vector<vector<char>>& board, int r, int c, TrieNode* node, vector<string>& res) {
char ch = board[r][c];
if (ch == '#' || !node->children[ch - 'a']) return;
node = node->children[ch - 'a'];
if (node->word != "") {
res.push_back(node->word);
node->word = "";
}
board[r][c] = '#';
int dirs[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
for (auto& d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < board.size() && nc >= 0 && nc < board[0].size())
dfs(board, nr, nc, node, res);
}
board[r][c] = ch;
}
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
TrieNode* root = new TrieNode();
for (auto& word : words) insert(root, word);
vector<string> res;
for (int r = 0; r < board.size(); ++r)
for (int c = 0; c < board[0].size(); ++c)
dfs(board, r, c, root, res);
return res;
}
};
class TrieNode {
TrieNode[] children = new TrieNode[26];
String word = null;
}
class Solution {
public List<String> findWords(char[][] board, String[] words) {
TrieNode root = new TrieNode();
for (String word : words) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (node.children[idx] == null) node.children[idx] = new TrieNode();
node = node.children[idx];
}
node.word = word;
}
List<String> res = new ArrayList<>();
int rows = board.length, cols = board[0].length;
for (int r = 0; r < rows; ++r)
for (int c = 0; c < cols; ++c)
dfs(board, r, c, root, res);
return res;
}
private void dfs(char[][] board, int r, int c, TrieNode node, List<String> res) {
char ch = board[r][c];
if (ch == '#' || node.children[ch - 'a'] == null) return;
node = node.children[ch - 'a'];
if (node.word != null) {
res.add(node.word);
node.word = null;
}
board[r][c] = '#';
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < board.length && nc >= 0 && nc < board[0].length)
dfs(board, nr, nc, node, res);
}
board[r][c] = ch;
}
}
class TrieNode {
constructor() {
this.children = {};
this.word = null;
}
}
var findWords = function(board, words) {
const root = new TrieNode();
for (const word of words) {
let node = root;
for (const char of word) {
if (!node.children[char]) node.children[char] = new TrieNode();
node = node.children[char];
}
node.word = word;
}
const res = [];
const rows = board.length, cols = board[0].length;
function dfs(r, c, node) {
const char = board[r][c];
if (!node.children[char]) return;
const nextNode = node.children[char];
if (nextNode.word) {
res.push(nextNode.word);
nextNode.word = null;
}
board[r][c] = '#';
for (const [dr, dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && board[nr][nc] !== '#') {
dfs(nr, nc, nextNode);
}
}
board[r][c] = char;
}
for (let r = 0; r < rows; ++r)
for (let c = 0; c < cols; ++c)
dfs(r, c, root);
return res;
};
Given a 2D board of characters called board
and a list of strings called words
, find all words from words
that can be formed by sequentially adjacent letters on the board. Each letter cell may only be used once per word.
For example, if board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]]
and words = ["oath","pea","eat","rain"]
, the output should be ["eat","oath"]
.
At first glance, this problem seems similar to a word search puzzle. For each word, you could try to find a path on the board that spells it out, moving up, down, left, or right, and not revisiting any cell for the same word.
The naive approach would be to search for each word independently, starting from every cell and using depth-first search (DFS) to try to spell out the word. However, this quickly becomes inefficient if the board or the word list is large, as we might repeat a lot of the same searches.
To optimize, we notice that many words may share common prefixes. For example, “eat” and “ear” both start with “ea”. Instead of searching for each word separately, we can search for all words simultaneously by building a Trie (prefix tree) from the word list. This way, we only need to explore each path on the board once, and we can check for any word that starts with the current prefix.
The key insight is to combine DFS traversal of the board with prefix matching using the Trie. This avoids repeated work and efficiently finds all matches.
words
list into a Trie data structure. Each node in the Trie represents a letter, and a complete word is marked at the last letter node.board
, start a DFS traversal. At each step, check if the current letter exists as a child in the Trie node.'#'
).We use a Trie for fast prefix lookups (O(1) per character), and DFS to explore the board efficiently. This avoids redundant searches and ensures we only find valid words.
Let's use the example:
board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]]
words = ["oath","pea","eat","rain"]
Step 1: Build a Trie with the four words.
Step 2: Start DFS from each cell. Let's say we start at cell (0,0) which is 'o'. We look for words starting with 'o' in the Trie.
Step 3: From (0,0), we can move to (1,0) which is 'e', but 'oe' is not a prefix in the Trie, so we backtrack. Instead, from (0,0), we move to (0,1) which is 'a', forming 'oa', which is a prefix for "oath".
Step 4: Continue DFS, visiting neighboring cells and matching the Trie. Eventually, we spell out "oath" by visiting the path (0,0) → (0,1) → (1,1) → (2,1).
Step 5: Mark "oath" as found, and continue DFS from other starting cells.
Step 6: Similarly, we find "eat" by starting from (1,0) → (1,1) → (1,2).
Step 7: "pea" and "rain" are not found on the board.
The final result is ["oath","eat"]
.
The use of the Trie significantly reduces redundant searches, making the algorithm efficient for large boards and word lists.
By combining a Trie for fast prefix checking with DFS for board traversal, we efficiently solve the Word Search II problem. This approach avoids redundant searches by sharing common prefixes and only explores valid paths, making it both elegant and performant. The key insight is to treat the search as a simultaneous exploration for all words, leveraging the Trie to prune dead ends early and collect results as soon as they are found.