The Problem
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.
Examples
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"]
Input: board = [["a","b"],["c","d"]], words = ["abcb"]
Output: []
Constraints
- m == board.length
- n == board[i].length
- 1 <= m, n <= 12
- board[i][j] is a lowercase English letter.
- 1 <= words.length <= 3 * 10^4
- 1 <= words[i].length <= 10
- words[i] consists of lowercase English letters.
- All the strings in
words
are unique.
Brute Force Solution
func findWords(_ board: [[Character]], _ words: [String]) -> [String] {
var board = board
let ROWS = board.count
let COLS = board[0].count
var res: [String] = []
func backtrack(_ r: Int, _ c: Int, _ word: [Character], _ i: Int) -> Bool {
if i == word.count {
return true
}
if (r < 0 || c < 0 || r >= ROWS ||
c >= COLS || board[r][c] != word[i]
) {
return false
}
let tmp = board[r][c]
board[r][c] = "*"
let res = (backtrack(r + 1, c, word, i + 1) ||
backtrack(r - 1, c, word, i + 1) ||
backtrack(r, c + 1, word, i + 1) ||
backtrack(r, c - 1, word, i + 1))
board[r][c] = tmp
return res
}
for word in words {
let wordArray = Array(word)
var flag = false
for r in 0 ..< ROWS {
if flag {
break
}
for c in 0 ..< COLS {
if board[r][c] != wordArray[0] {
continue
}
if backtrack(r, c, wordArray, 0) {
res.append(word)
flag = true
break
}
}
}
}
return res
}
Explanation
We can solve this problem using a brute-force approach with a backtracking (depth-first search) algorithm. This algorithm allows us to visit neighboring cells.
According to the problem description, we need to construct words from characters available on the board
that match the input words
.
The brute-force approach involves visiting each cell on the board and backtracking in four directions to try to construct possible words until we reach the bottom of the grid.
However, this is not an efficient solution and will take O(m * n * 4^t + s). We can optimize this solution by using a Trie (Prefix Tree).
Time/Space Complexity
- Time complexity: O(m * n * 4^t + s)
- Space complexity: O(t)
- where
m
is the number of rows,n
is the number of columns,t
is the maximum length of any word in thewords
array, ands
is the sum of the lengths of all words.
Trie (Prefix Tree) Solution
final class TrieNode {
var children: [Character: TrieNode]
var endOfWord: Bool
init() {
self.children = [:]
self.endOfWord = false
}
func addWord(_ word: String) {
var curr = self
for c in word {
if curr.children[c] == nil {
curr.children[c] = TrieNode()
}
curr = curr.children[c]!
}
curr.endOfWord = true
}
}
func findWords(_ board: [[Character]], _ words: [String]) -> [String] {
var root = TrieNode()
for w in words {
root.addWord(w)
}
let ROWS = board.count
let COLS = board[0].count
var res: Set<String> = []
var visit: Set<[Int]> = []
func dfs(_ r: Int, _ c: Int, _ node: TrieNode?, _ word: String) {
var word = word
if (r < 0 || c < 0 || r >= ROWS ||
c >= COLS || visit.contains([r, c]) ||
node?.children[board[r][c]] == nil
) {
return
}
visit.insert([r, c])
let node = node?.children[board[r][c]]
word += String(board[r][c])
if node!.endOfWord {
res.insert(word)
}
dfs(r + 1, c, node, word)
dfs(r - 1, c, node, word)
dfs(r, c + 1, node, word)
dfs(r, c - 1, node, word)
visit.remove([r, c])
}
for r in 0 ..< ROWS {
for c in 0 ..< COLS {
dfs(r, c, root, "")
}
}
return Array(res)
}
Explanation
We can optimize the brute-force solution using a Trie (Prefix Tree) data structure. To do this, we need a TrieNode
class with an addWord
helper method. For the findWords
function, we need an additional depth-first search (backtracking) algorithm. The TrieNode
will help us store words, while DFS will help us search for words on the board in four directions.
In a Prefix Tree, every single character is a node. We insert all input words into the Prefix Tree. For example, with words = ["app", "ape", "ace"]
:
After constructing the tree, we search for prefixes. If a prefix exists, we continue searching until we find a word or reach a base case. As a result, we find two words ["ape", "ace"]
that can be built using the given board.
Time/Space Complexity
- Time complexity: O(m * n * 4 * 3^(t-1) + s)
- Space complexity: O(s)
- where
m
is the number of rows,n
is the number of columns,t
is the maximum length of any word inwords
, ands
is the sum of the lengths of all words.