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);
        }
    }
    
  • 相关阅读:
    pta ——还原二叉树
    pta ——Topological Sort(拓扑排序)
    pta Two Stacks In One Array(简单版双向数组)
    pta Percolate Up and Down(最小堆的插入维护和删除维护)
    pta 切分表达式——写个tokenizer吧
    正则表达式基本语法详解
    pat 是否同一棵二叉搜索树
    关于java中字符、字符串、整数相加输出的小整理
    纯C语言实现的AVL树(插入&删除&前序遍历输出)
    pat Deque(双端队列)
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14774021.html
Copyright © 2011-2022 走看看