zoukankan      html  css  js  c++  java
  • Hard | LeetCode 212. 单词搜索 II | 回溯 + 前缀树

    212. 单词搜索 II

    给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。

    单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

    示例 1:

    img
    输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
    输出:["eat","oath"]
    

    示例 2:

    img
    输入:board = [["a","b"],["c","d"]], words = ["abcb"]
    输出:[]
    

    提示:

    • m == board.length
    • n == board[i].length
    • 1 <= m, n <= 12
    • board[i][j] 是一个小写英文字母
    • 1 <= words.length <= 3 * 104
    • 1 <= words[i].length <= 10
    • words[i] 由小写英文字母组成
    • words 中的所有字符串互不相同

    解题思路

    方法一:回溯 + 前缀树优化

    这道题和常见的DFS搜索题目类似。

    比如 Medium | LeetCode 200. 岛屿数量 | 矩阵 + DFS 这道题DFS的条件是, 下一个节点必须是1(陆地)
    比如 Hard | LeetCode 329. 矩阵中的最长递增路径 | 矩阵+DFS , 这道题DFS的条件是, 下一个节点值必须大于当前节点值

    而在本题当中, DFS的条件是 当前节点在某个单词的路径上。

    解决这个的思路可以是把从DFS起点到当前的所有字符取出来, 然后判断是否是单词列表的某个单词的前缀。这种方法可行, 但是复杂度很高。

    有一种优化的方法:前缀树 (Medium | LeetCode 208. 实现 Trie (前缀树) | 设计)

    在DFS的过程当中, 让矩阵当前位置 和 前缀树的节点 对应起来, 就很容易知道 下一个节点是否在某个单词的路径上。当DFS到矩阵的某个位置, 其对应的在前缀树的节点 是代表一个单词的时候, 就将其加入到结果集当中。

    同时在将某个单词添加进结果集的时候, 可以将前缀树的word标志置为null, 因为这个单词已经加进去了, 不会再添加了。
    如果这个被添加进结果集的单词的节点是叶节点的时候, 可以删除这个单词的路径上的节点。这样可以提高效率。

    代码

    前缀树的数据结构编码

    static class TrieTree {
    
        static class TrieNode {
            // 用来保存当前节点的所有孩子节点的字符以及节点映射信息
            private HashMap<Character, TrieNode> children = new HashMap<>();
            // 用来表示当前节点是否代表一个单词
            private String word;
    
            public void setWord(String word) {
                this.word = word;
            }
    
            private boolean containCharChild(Character c) {
                return children.containsKey(c);
            }
    
            private TrieNode getCharChild(Character c) {
                return children.get(c);
            }
    
            private TrieNode addCharChild(Character c) {
                TrieNode rtn  = new TrieNode();
                children.put(c, rtn);
                return rtn;
            }
        }
    
        private TrieNode root;
    
        public TrieTree() {
            this.root = new TrieNode();
        }
    
        public TrieNode getRoot() {
            return root;
        }
    
        public void addWord(String word) {
            char[] wordArray = word.toCharArray();
            TrieNode cur = root;
            for (char c : wordArray) {
                if (cur.containCharChild(c)) {
                    cur = cur.getCharChild(c);
                } else {
                    cur = cur.addCharChild(c);
                }
            }
            cur.setWord(word);
        }
    
        public void removeWord(String word) {
    
        }
    
        public void show() {
            dfsShowCore(root);
        }
    
        private void dfsShowCore(TrieNode parents) {
            if (parents.children.isEmpty()) {
                System.out.println();
            }
            for (Entry<Character, TrieNode> trieNodeEntry : parents.children.entrySet()) {
                TrieNode node = trieNodeEntry.getValue();
                System.out.print(trieNodeEntry.getKey());
                if (node.word != null) {
                    System.out.print(String.format(" (%s)", node.word));
                }
                if (!node.children.isEmpty()) {
                    System.out.print(" -> ");
                }
                dfsShowCore(node);
            }
        }
    }
    

    代码主体部分

    public List<String> findWords(char[][] board, String[] words) {
        // 先根据单词列表构造前缀树
        TrieTree trie = new TrieTree();
        for (String word : words) {
            trie.addWord(word);
        }
        trie.show();
        TrieNode root = trie.getRoot();
        List<String> res = new ArrayList<>();
        for (int row = 0; row < board.length; ++row) {
            for (int col = 0; col < board[row].length; ++col) {
                if (root.containCharChild(board[row][col])) {
                    backtracking(board, row, col, root, res);
                }
            }
        }
        return res;
    }
    
    private void backtracking(char[][] board, int row, int col, TrieNode parent, List<String> res) {
        Character letter = board[row][col];
        TrieNode curNode = parent.getCharChild(letter);
    
        // 找到一个单词
        if (curNode.word != null) {
            res.add(curNode.word);
            // 这个单词已经加进结果集了, 不会添加重复的单词, 所以将当前节点代表的单词删除
            curNode.word = null;
        }
    
        // 保证每一次枚举过程, 这个位置的字符不会重复使用, 使用"#"号标记, 这轮枚举结束之后回溯时, 会将其还原。
        board[row][col] = '#';
    
        // DFS遍历其他相邻的位置
        int[] rowOffset = {-1, 0, 1, 0};
        int[] colOffset = {0, 1, 0, -1};
        for (int i = 0; i < 4; ++i) {
            int newRow = row + rowOffset[i];
            int newCol = col + colOffset[i];
            if (newRow < 0 || newRow >= board.length || newCol < 0
                    || newCol >= board[0].length) {
                continue;
            }
            if (curNode.containCharChild(board[newRow][newCol])) {
                backtracking(board, newRow, newCol, curNode, res);
            }
        }
    
        // 还原初始字母
        board[row][col] = letter;
    
        // 对于 Trie 中的叶节点,一旦遍历它(即找到匹配的单词),就不需要再遍历它了, 所以可以直接删除
        // 如果删除某个不是单词的叶节点, 那么遍历到这个叶节点也没有意义了, 同样删除掉。
        if (curNode.children.isEmpty()) {
            parent.children.remove(letter);
        }
    }
    
  • 相关阅读:
    面向对象的继承关系体现在数据结构上时,如何表示
    codeforces 584C Marina and Vasya
    codeforces 602A Two Bases
    LA 4329 PingPong
    codeforces 584B Kolya and Tanya
    codeforces 584A Olesya and Rodion
    codeforces 583B Robot's Task
    codeforces 583A Asphalting Roads
    codeforces 581C Developing Skills
    codeforces 581A Vasya the Hipster
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14774021.html
Copyright © 2011-2022 走看看