{x}
blog image

Word Search II

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.length
  • n == board[i].length
  • 1 <= m, n <= 12
  • board[i][j] is a lowercase English letter.
  • 1 <= words.length <= 3 * 104
  • 1 <= words[i].length <= 10
  • words[i] consists of lowercase English letters.
  • All the strings of words are unique.

Solution Explanation:

This problem asks to find all words in a given board of characters that are present in a list of words. The words can be formed by connecting adjacent cells horizontally or vertically, without reusing a cell. The solution employs a Trie data structure for efficient word searching.

Approach:

  1. Trie Construction: A Trie is built from the list of words. Each node in the Trie represents a character, and paths from the root to a leaf node represent words. Each leaf node stores an index pointing to the corresponding word in the input list. This allows for fast prefix-based searching.

  2. Depth-First Search (DFS): A DFS function explores the board. It starts at each cell of the board and recursively searches for words. During the search:

    • It checks if the current character exists as a child in the current Trie node.
    • If a word is found (leaf node reached), it's added to the result list. To avoid duplicates, the word's reference in the Trie is marked as used.
    • The current cell is marked as visited ('#') to prevent reuse.
    • The search continues recursively to adjacent cells.
    • After the recursion, the current cell is un-marked to allow exploration from other paths.
  3. Result: The function returns a list of all words found on the board.

Code Explanation (Python):

class Trie:
    def __init__(self):
        self.children = [None] * 26  # Array to store children nodes (a-z)
        self.ref = -1  # Index of the word in the words list (-1 if not a word)
 
    def insert(self, w: str, ref: int):
        node = self
        for c in w:
            idx = ord(c) - ord('a')  # Get index from 'a' to 'z'
            if node.children[idx] is None:
                node.children[idx] = Trie()
            node = node.children[idx]
        node.ref = ref #Mark leaf node with word index
 
class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        #Build Trie
        tree = Trie()
        for i, w in enumerate(words):
            tree.insert(w, i)
 
        m, n = len(board), len(board[0])
        ans = []
 
        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  # Mark as used to avoid duplicates
 
            c = board[i][j]
            board[i][j] = '#'  # Mark as visited
            for a, b in pairwise((-1, 0, 1, 0, -1)): #Directions
                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  # Unmark
 
        for i in range(m):
            for j in range(n):
                dfs(tree, i, j)
        return ans
 
from itertools import pairwise #helper function for directions

The code in other languages follows a similar structure. pairwise is a helper function from itertools to easily generate movement directions.

Time Complexity Analysis:

  • Trie Construction: O(W), where W is the total number of characters in all words. Inserting a word into the Trie takes linear time proportional to its length.

  • DFS: In the worst case, the DFS explores all possible paths on the board. The number of paths depends on the size of the board (M x N) and the length of words. It's roughly O(M * N * 4L) where L is the maximum length of a word. However, the Trie significantly reduces the number of paths explored by eliminating early on prefixes that don't match any word.

  • Overall: The overall time complexity is dominated by the DFS, making it approximately O(M * N * 4L) in the worst case. However, due to the Trie and the early termination of paths that don't match word prefixes, the actual runtime is significantly better than this theoretical upper bound in most practical scenarios.

Space Complexity Analysis:

  • Trie: The Trie's space complexity is O(W), proportional to the total number of characters in the words.

  • DFS Recursion Stack: The depth of the recursion stack in the DFS is at most L (maximum word length).

  • Result List: The space used to store the result list is O(R), where R is the number of words found.

  • Overall: The overall space complexity is O(W + L + R). In the worst case, R can be as large as the number of cells in the board (M*N). Again, this is the upper bound; the actual space used will depend on the number of words found.