Off-by-one on range boundaries
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Break down a hard problem into reliable checkpoints, edge-case handling, and complexity trade-offs.
Given an m x n board of characters and a list of strings words, return all words on the board.
Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
Example 1:
Input: board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"] Output: ["eat","oath"]
Example 2:
Input: board = [["a","b"],["c","d"]], words = ["abcb"] Output: []
Constraints:
m == board.lengthn == board[i].length1 <= m, n <= 12board[i][j] is a lowercase English letter.1 <= words.length <= 3 * 1041 <= words[i].length <= 10words[i] consists of lowercase English letters.words are unique.Problem summary: Given an m x n board of characters and a list of strings words, return all words on the board. Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
Start with the most direct exhaustive search. That gives a correctness anchor before optimizing.
Pattern signal: Array · Backtracking · Trie
[["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]] ["oath","pea","eat","rain"]
[["a","b"],["c","d"]] ["abcb"]
word-search)unique-paths-iii)encrypt-and-decrypt-strings)Source-backed implementations are provided below for direct study and interview prep.
// Accepted solution for LeetCode #212: Word Search II
class Trie {
Trie[] children = new Trie[26];
int ref = -1;
public void insert(String w, int ref) {
Trie node = this;
for (int i = 0; i < w.length(); ++i) {
int j = w.charAt(i) - 'a';
if (node.children[j] == null) {
node.children[j] = new Trie();
}
node = node.children[j];
}
node.ref = ref;
}
}
class Solution {
private char[][] board;
private String[] words;
private List<String> ans = new ArrayList<>();
public List<String> findWords(char[][] board, String[] words) {
this.board = board;
this.words = words;
Trie tree = new Trie();
for (int i = 0; i < words.length; ++i) {
tree.insert(words[i], i);
}
int m = board.length, n = board[0].length;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
dfs(tree, i, j);
}
}
return ans;
}
private void dfs(Trie node, int i, int j) {
int idx = board[i][j] - 'a';
if (node.children[idx] == null) {
return;
}
node = node.children[idx];
if (node.ref != -1) {
ans.add(words[node.ref]);
node.ref = -1;
}
char c = board[i][j];
board[i][j] = '#';
int[] dirs = {-1, 0, 1, 0, -1};
for (int k = 0; k < 4; ++k) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x >= 0 && x < board.length && y >= 0 && y < board[0].length && board[x][y] != '#') {
dfs(node, x, y);
}
}
board[i][j] = c;
}
}
// Accepted solution for LeetCode #212: Word Search II
type Trie struct {
children [26]*Trie
ref int
}
func newTrie() *Trie {
return &Trie{ref: -1}
}
func (this *Trie) insert(w string, ref int) {
node := this
for _, c := range w {
c -= 'a'
if node.children[c] == nil {
node.children[c] = newTrie()
}
node = node.children[c]
}
node.ref = ref
}
func findWords(board [][]byte, words []string) (ans []string) {
trie := newTrie()
for i, w := range words {
trie.insert(w, i)
}
m, n := len(board), len(board[0])
var dfs func(*Trie, int, int)
dfs = func(node *Trie, i, j int) {
idx := board[i][j] - 'a'
if node.children[idx] == nil {
return
}
node = node.children[idx]
if node.ref != -1 {
ans = append(ans, words[node.ref])
node.ref = -1
}
c := board[i][j]
board[i][j] = '#'
dirs := [5]int{-1, 0, 1, 0, -1}
for k := 0; k < 4; k++ {
x, y := i+dirs[k], j+dirs[k+1]
if x >= 0 && x < m && y >= 0 && y < n && board[x][y] != '#' {
dfs(node, x, y)
}
}
board[i][j] = c
}
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
dfs(trie, i, j)
}
}
return
}
# Accepted solution for LeetCode #212: Word Search II
class Trie:
def __init__(self):
self.children: List[Trie | None] = [None] * 26
self.ref: int = -1
def insert(self, w: str, ref: int):
node = self
for c in w:
idx = ord(c) - ord('a')
if node.children[idx] is None:
node.children[idx] = Trie()
node = node.children[idx]
node.ref = ref
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
def dfs(node: Trie, i: int, j: int):
idx = ord(board[i][j]) - ord('a')
if node.children[idx] is None:
return
node = node.children[idx]
if node.ref >= 0:
ans.append(words[node.ref])
node.ref = -1
c = board[i][j]
board[i][j] = '#'
for a, b in pairwise((-1, 0, 1, 0, -1)):
x, y = i + a, j + b
if 0 <= x < m and 0 <= y < n and board[x][y] != '#':
dfs(node, x, y)
board[i][j] = c
tree = Trie()
for i, w in enumerate(words):
tree.insert(w, i)
m, n = len(board), len(board[0])
ans = []
for i in range(m):
for j in range(n):
dfs(tree, i, j)
return ans
// Accepted solution for LeetCode #212: Word Search II
#[derive(Default, Debug, Clone)]
struct Trie {
is_word_end: bool,
children: [Option<Box<Trie>>; 26],
}
impl Solution {
pub fn find_words(mut board: Vec<Vec<char>>, words: Vec<String>) -> Vec<String> {
let mut trie = Trie::new();
let (m, n) = (board.len(), board[0].len());
for word in words{
trie.insert(word);
}
let mut word = String::new();
let mut res = vec![];
for i in 0..m{
for j in 0..n{
Self::dfs(&mut board, &mut trie, &mut res, &mut word, i, j);
}
}
res
}
pub fn dfs(
board: &mut Vec<Vec<char>>,
mut trie: &mut Trie,
res: &mut Vec<String>,
word: &mut String,
i: usize,
j: usize
){
let (m, n) = (board.len(), board[0].len());
if i!= usize::MAX &&
j!= usize::MAX &&
i < m &&
j < n &&
board[i][j] != '#'
{
let c = board[i][j];
// mark as visited
board[i][j] = '#';
if let Some(ref mut curr_trie) = trie.children[char_index(c)]{
trie = curr_trie.as_mut();
word.push(c);
if trie.is_word_end{
res.push(word.clone());
trie.is_word_end = false;
}
for (x, y) in [(i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)]{
Self::dfs(board, trie, res, word, x, y);
}
word.pop();
}
board[i][j] = c;
}
}
}
impl Trie {
fn new() -> Self {
Default::default()
}
fn insert(&mut self, word: String) {
let mut trie = self;
for c in word.chars(){
trie = trie.children[char_index(c)]
.get_or_insert(Box::new(Trie::new()))
.as_mut();
}
trie.is_word_end = true;
}
}
pub fn char_index(c: char) -> usize{
c as usize - 'a' as usize
}
// Accepted solution for LeetCode #212: Word Search II
class Trie {
children: Trie[];
ref: number;
constructor() {
this.children = new Array(26);
this.ref = -1;
}
insert(w: string, ref: number): void {
let node: Trie = this;
for (let i = 0; i < w.length; i++) {
const c = w.charCodeAt(i) - 97;
if (node.children[c] == null) {
node.children[c] = new Trie();
}
node = node.children[c];
}
node.ref = ref;
}
}
function findWords(board: string[][], words: string[]): string[] {
const tree = new Trie();
for (let i = 0; i < words.length; ++i) {
tree.insert(words[i], i);
}
const m = board.length;
const n = board[0].length;
const ans: string[] = [];
const dirs: number[] = [-1, 0, 1, 0, -1];
const dfs = (node: Trie, i: number, j: number) => {
const idx = board[i][j].charCodeAt(0) - 97;
if (node.children[idx] == null) {
return;
}
node = node.children[idx];
if (node.ref != -1) {
ans.push(words[node.ref]);
node.ref = -1;
}
const c = board[i][j];
board[i][j] = '#';
for (let k = 0; k < 4; ++k) {
const x = i + dirs[k];
const y = j + dirs[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] != '#') {
dfs(node, x, y);
}
}
board[i][j] = c;
};
for (let i = 0; i < m; ++i) {
for (let j = 0; j < n; ++j) {
dfs(tree, i, j);
}
}
return ans;
}
Use this to step through a reusable interview workflow for this problem.
Generate every possible combination without any filtering. At each of n positions we choose from up to n options, giving nⁿ total candidates. Each candidate takes O(n) to validate. No pruning means we waste time on clearly invalid partial solutions.
Backtracking explores a decision tree, but prunes branches that violate constraints early. Worst case is still factorial or exponential, but pruning dramatically reduces the constant factor in practice. Space is the recursion depth (usually O(n) for n-level decisions).
Review these before coding to avoid predictable interview regressions.
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Wrong move: Mutable state leaks between branches.
Usually fails on: Later branches inherit selections from earlier branches.
Fix: Always revert state changes immediately after recursive call.