212. Word Search II

11 阅读2分钟

image.png

Solution: DFS + Trie

  • The key is how we find the matches of word from the dictionary. We can use hashset
  • But during dfs/backtracking, we need to tell if there exists any word that contains certain prefix.
  • Because if we know that there does not exist any match of word in the dictionary for a given prefix, then we would not need to further explore certain direction.

Steps:

  • We build a Trie out of the words in the dictionary, which would be used for the matching process later.
  • Starting from each cell, we start the backtracking exploration if there exists any word in the dictionary that starts with the letter in the cell.
  • During the recursive call, we explore the neighbor cells for the next recursive call. At each call, we check if the sequence of letters that we traverse so far matches any word in the dictionary/Trie.

image.png

Time complexity: O(M * (4⋅3^(L−1))) -> o(M * 3^L)

where M is the number of cells in the board and L is the maximum length of words.

class Solution {
    List<String> res;

    public List<String> findWords(char[][] board, String[] words) {
        int m = board.length, n = board[0].length;
        res = new ArrayList<>();

        // root is dummy
        Node root = buildTrie(words);
    
        boolean[][] visited = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j <n; j++) {
                if (root.children.containsKey(board[i][j])) {
                    dfs(board, visited, i, j, root); // root is dummy, treated as parent
                }
            }
        }
        return res;
    }

    // Trie node
    class Node {
        Map<Character, Node> children = new HashMap<>();
        // if non-null, indicates the end of word (leaf node of Trie)
        String word = null; 
        public Node() {}
    }

    // Build the Trie, return the dummy root
    public Node buildTrie(String[] words) {
        Node root = new Node(); // root of multiple words

        for (String word : words) {
            Node cur = root;
            for (char c : word.toCharArray()) {
                if (!cur.children.containsKey(c)) {
                    cur.children.put(c, new Node());
                }
                cur = cur.children.get(c); // move pointer down level
            }
            cur.word = word; // end of word
        }
        return root;
    }

    public void dfs(char[][] board, boolean[][] visited, int i, int j, Node parent) {
        int m = board.length, n = board[0].length;
        if (i >= m || i < 0 || j >= n || j < 0) {
            return;
        }
        
        // Can't go back
        if (visited[i][j]) {
            return;
        }

        // cur char on board DOESNT match cur char in Trie
        Node cur = parent.children.get(board[i][j]);
        if (cur == null) {
            return;
        }

        // Found target word on board
        if (cur.word != null) {
            res.add(cur.word);
            // Clear the word so we don't report it again if we come back to this node.
            cur.word = null; // avoid duplicate
        }

        visited[i][j] = true; // better to add here (after the above return)

        // 4 directions dfs
        dfs(board, visited, i + 1, j, cur);
        dfs(board, visited, i - 1, j, cur);
        dfs(board, visited, i, j + 1, cur);
        dfs(board, visited, i, j - 1, cur);

        // Optimization: incrementally remove the leaf nodes
        // For a leaf node in Trie, once we traverse it (i.e. find a matched word), 
        // we would no longer need to traverse it again.
        //  As a result, we could prune it out from the Trie.
        // if (cur.children.isEmpty()) {
        //     parent.children.remove(board[i][j]);
        // }

        visited[i][j] = false;
    }
}