|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[676. 实现一个魔法字典](https://leetcode.cn/problems/cut-off-trees-for-golf-event/solution/by-ac_oier-ksth/)** ,难度为 **中等**。 |
| 4 | + |
| 5 | +Tag : 「字典树」、「DFS」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于你构建的字典中。 |
| 10 | + |
| 11 | +实现 `MagicDictionary` 类: |
| 12 | + |
| 13 | +* `MagicDictionary()` 初始化对象 |
| 14 | +* `void buildDict(String[] dictionary)` 使用字符串数组 `dictionary` 设定该数据结构,`dictionary` 中的字符串互不相同 |
| 15 | +* `bool search(String searchWord)` 给定一个字符串 `searchWord`,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 `true`;否则,返回 `false`。 |
| 16 | + |
| 17 | +示例: |
| 18 | +``` |
| 19 | +输入 |
| 20 | +["MagicDictionary", "buildDict", "search", "search", "search", "search"] |
| 21 | +[[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]] |
| 22 | + |
| 23 | +输出 |
| 24 | +[null, null, false, true, false, false] |
| 25 | + |
| 26 | +解释 |
| 27 | +MagicDictionary magicDictionary = new MagicDictionary(); |
| 28 | +magicDictionary.buildDict(["hello", "leetcode"]); |
| 29 | +magicDictionary.search("hello"); // 返回 False |
| 30 | +magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello" ,所以返回 True |
| 31 | +magicDictionary.search("hell"); // 返回 False |
| 32 | +magicDictionary.search("leetcoded"); // 返回 False |
| 33 | +``` |
| 34 | + |
| 35 | +提示: |
| 36 | +* 1ドル <= dictionary.length <= 100$ |
| 37 | +* 1ドル <= dictionary[i].length <= 100$ |
| 38 | +* `dictionary[i]` 仅由小写英文字母组成 |
| 39 | +* `dictionary` 中的所有字符串 互不相同 |
| 40 | +* 1ドル <= searchWord.length <= 100$ |
| 41 | +* `searchWord` 仅由小写英文字母组成 |
| 42 | +* `buildDict` 仅在 `search` 之前调用一次 |
| 43 | +* 最多调用 100ドル$ 次 `search` |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +### Trie + DFS |
| 48 | + |
| 49 | +为了方便,我们令 `dictionary` 为 `ss`,令 `searchWord` 为 `s`。 |
| 50 | + |
| 51 | +整体题意:给定字符串 `s`,问能否存在替换掉 `s` 中的某个字符,使得新字符串出现在 `ss` 数组中。 |
| 52 | + |
| 53 | +考虑如何使用「字典树/`Trie`」求解该问题: |
| 54 | +* `buildDict` 操作:我们可以将所有的 $ss[i]$ 存入字典树中,方便后续检索; |
| 55 | + |
| 56 | +* `search` 操作:设计递归函数 `boolean query(String s, int idx, int p, int limit)`,其中 `s` 为待检索的字符串,`idx` 为当前处理到字符串 `s` 的哪一位,`p` 为当前搜索到字典树的索引编号(起始有 $p = 0$),`limit` 为当前剩余的替换字符次数,根据题意,`limit` 固定为 1ドル,ドル含义为必须替换掉 `s` 的一个字符。 |
| 57 | + 对于 $s[idx]$ 而言,我们可以枚举新字符串在当前位置是何种字符($C = 26$ 个选择),若当前枚举到的字符与 $s[idx]$ 一致,则不消耗替换次数。 |
| 58 | + 爆搜过程中替换次数为负数直接剪枝,当爆搜到结尾位置,再检查当前的字典树索引 $p$ 是否为单词结尾节点(对应查询数组 `ss` 中是否存在该字符串),以及剩余的替换次数 `limit` 是否为 0ドル$。 |
| 59 | + |
| 60 | +> **不了解「Trie / 字典树」的同学可以看前置 🧀:[字典树入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)。里面通过图例展示了字典树基本形态,以及提供了「数组实现」和「TrieNode 实现」两种方式,还有「数组大小估算方式」和「Trie 应用面」介绍** |
| 61 | + |
| 62 | +代码: |
| 63 | +```Java |
| 64 | +class MagicDictionary { |
| 65 | + int N = 100 * 100, M = 26, idx = 0; |
| 66 | + int[][] tr = new int[N][M]; |
| 67 | + boolean[] isEnd = new boolean[N * M]; |
| 68 | + void add(String s) { |
| 69 | + int p = 0; |
| 70 | + for (int i = 0; i < s.length(); i++) { |
| 71 | + int u = s.charAt(i) - 'a'; |
| 72 | + if (tr[p][u] == 0) tr[p][u] = ++idx; |
| 73 | + p = tr[p][u]; |
| 74 | + } |
| 75 | + isEnd[p] = true; |
| 76 | + } |
| 77 | + boolean query(String s, int idx, int p, int limit) { |
| 78 | + if (limit < 0) return false; |
| 79 | + if (idx == s.length()) return isEnd[p] && limit == 0; |
| 80 | + int u = s.charAt(idx) - 'a'; |
| 81 | + for (int i = 0; i < 26; i++) { |
| 82 | + if (tr[p][i] == 0) continue; |
| 83 | + if (query(s, idx + 1, tr[p][i], i == u ? limit : limit - 1)) return true; |
| 84 | + } |
| 85 | + return false; |
| 86 | + } |
| 87 | + public void buildDict(String[] ss) { |
| 88 | + for (String s : ss) add(s); |
| 89 | + } |
| 90 | + public boolean search(String s) { |
| 91 | + return query(s, 0, 0, 1); |
| 92 | + } |
| 93 | +} |
| 94 | +``` |
| 95 | +* 时间复杂度:`buildDict` 操作需要将所有字符存入 `Trie`,复杂度为 $\sum_{i = 0}^{n - 1} len(ss[i]])$;`search` 操作在不考虑 `limit` 以及字典树中最多只有 100ドル$ 具体方案所带来的剪枝效果的话,最坏情况下要搜索所有 $C^L$ 个方案,其中 $C = 26$ 为字符集大小,$L = 100$ 为搜索字符串的最大长度 |
| 96 | +* 空间复杂度:$O(N \times L \times C),ドル其中 $N = 100$ 为存入 `Trie` 的最大方案数,$L = 100$ 为存入字符串的最大长度,$C = 26$ 为字符集大小 |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +### 最后 |
| 101 | + |
| 102 | +这是我们「刷穿 LeetCode」系列文章的第 `No.676` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 103 | + |
| 104 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 105 | + |
| 106 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 107 | + |
| 108 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 109 | + |
0 commit comments