|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1032. 字符流](https://leetcode.cn/problems/stream-of-characters/solution/by-ac_oier-ihd4/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「字典树」、「枚举」、「剪枝」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +设计一个算法:接收一个字符流,并检查这些字符的后缀是否是字符串数组 `words` 中的一个字符串。 |
| 10 | + |
| 11 | +例如,`words = ["abc", "xyz"]` 且字符流中逐个依次加入 4ドル$ 个字符 `'a'`、`'x'`、`'y'` 和 `'z'` ,你所设计的算法应当可以检测到 `"axyz"` 的后缀 `"xyz"` 与 `words` 中的字符串 `"xyz"` 匹配。 |
| 12 | + |
| 13 | +按下述要求实现 `StreamChecker` 类: |
| 14 | + |
| 15 | +* `StreamChecker(String[] words)` :构造函数,用字符串数组 `words` 初始化数据结构。 |
| 16 | +* `boolean query(char letter)`:从字符流中接收一个新字符,如果字符流中的任一非空后缀能匹配 `words` 中的某一字符串,返回 `true`;否则,返回 `false`。 |
| 17 | + |
| 18 | +示例: |
| 19 | +``` |
| 20 | +输入: |
| 21 | +["StreamChecker", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query"] |
| 22 | +[[["cd", "f", "kl"]], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["i"], ["j"], ["k"], ["l"]] |
| 23 | + |
| 24 | +输出: |
| 25 | +[null, false, false, false, true, false, true, false, false, false, false, false, true] |
| 26 | + |
| 27 | +解释: |
| 28 | +StreamChecker streamChecker = new StreamChecker(["cd", "f", "kl"]); |
| 29 | +streamChecker.query("a"); // 返回 False |
| 30 | +streamChecker.query("b"); // 返回 False |
| 31 | +streamChecker.query("c"); // 返回n False |
| 32 | +streamChecker.query("d"); // 返回 True ,因为 'cd' 在 words 中 |
| 33 | +streamChecker.query("e"); // 返回 False |
| 34 | +streamChecker.query("f"); // 返回 True ,因为 'f' 在 words 中 |
| 35 | +streamChecker.query("g"); // 返回 False |
| 36 | +streamChecker.query("h"); // 返回 False |
| 37 | +streamChecker.query("i"); // 返回 False |
| 38 | +streamChecker.query("j"); // 返回 False |
| 39 | +streamChecker.query("k"); // 返回 False |
| 40 | +streamChecker.query("l"); // 返回 True ,因为 'kl' 在 words 中 |
| 41 | +``` |
| 42 | + |
| 43 | +提示: |
| 44 | +* 1ドル <= words.length <= 2000$ |
| 45 | +* 1ドル <= words[i].length <= 200$ |
| 46 | +* `words[i]` 由小写英文字母组成 |
| 47 | +* `letter` 是一个小写英文字母 |
| 48 | +* 最多调用查询 4ドル \times 10^4$ 次 |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +### Trie + 枚举 |
| 53 | + |
| 54 | +先考虑最为简单的做法:将给定的所有 $words[i]$ 顺序插入字典树,根据数据范围可知这一步计算量为 2000ドル \times 200,ドル其中最大的 $words[i]$ 长度只有 200ドル$。 |
| 55 | + |
| 56 | +然后利用$words[i]$ 长度只有 200ドル$ 这一条件,直接使用「枚举」的方式来实现 `query`。 |
| 57 | + |
| 58 | +具体的,我们可以先使用一个字符串 `s` 来记录 `query` 操作产生的数据流,然后实现一个 `boolean query(int start, int end)` 方法,该方法会检查字典树中是否存在 $s[i...j]$ 子串。 |
| 59 | + |
| 60 | +由于 $words[i]$ 长度只有 200ドル$(假设当前 `s` 的长度为 $n$),因此我们只需要枚举「$\max(0, n - 200)$ 作为子串左端点,$n - 1$ 作为子串右端点」是否存在字典树中(是否存在 $words[i]$ 中)即可,最坏情况下,单次 `query` 操作计算量为 200ドル \times 200$。 |
| 61 | + |
| 62 | +> 一些细节:为了避免每个样例都 `new` 大数组,我们可以使用 `static` 优化。 |
| 63 | + |
| 64 | +代码: |
| 65 | +```Java |
| 66 | +class StreamChecker { |
| 67 | + static int N = 2010 * 200, idx = 0; |
| 68 | + static int[][] tr = new int[N][26]; |
| 69 | + static boolean[] isEnd = new boolean[N * 26]; |
| 70 | + StringBuilder sb = new StringBuilder(); |
| 71 | + void add(String s) { |
| 72 | + int p = 0; |
| 73 | + for (int i = 0; i < s.length(); i++) { |
| 74 | + int u = s.charAt(i) - 'a'; |
| 75 | + if (tr[p][u] == 0) tr[p][u] = ++idx; |
| 76 | + p = tr[p][u]; |
| 77 | + } |
| 78 | + isEnd[p] = true; |
| 79 | + } |
| 80 | + boolean query(int start, int end) { |
| 81 | + int p = 0; |
| 82 | + for (int i = start; i <= end; i++) { |
| 83 | + int u = sb.charAt(i) - 'a'; |
| 84 | + if (tr[p][u] == 0) return false; |
| 85 | + p = tr[p][u]; |
| 86 | + } |
| 87 | + return isEnd[p]; |
| 88 | + } |
| 89 | + public StreamChecker(String[] words) { |
| 90 | + for (int i = 0; i <= idx; i++) { |
| 91 | + Arrays.fill(tr[i], 0); |
| 92 | + isEnd[i] = false; |
| 93 | + } |
| 94 | + idx = 0; |
| 95 | + for (String s : words) add(s); |
| 96 | + } |
| 97 | + public boolean query(char c) { |
| 98 | + sb.append(c); |
| 99 | + int n = sb.length(), min = Math.max(0, n - 200); |
| 100 | + for (int i = n - 1; i >= min; i--) { |
| 101 | + if (query(i, n - 1)) return true; |
| 102 | + } |
| 103 | + return false; |
| 104 | + } |
| 105 | +} |
| 106 | +``` |
| 107 | +* 时间复杂度:`StreamChecker` 初始化复杂度为 $O(n),ドル其中 $n$ 为 `words` 字符总数;`query` 操作复杂度为 $O(m^2),ドル其中 $m = 200$ 为最大 `words[i]` 长度 |
| 108 | +* 空间复杂度:$O(n \times C),ドル其中 $n$ 为 `words` 字符总数,$C = 26$ 为字符集大小 |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +### Trie(优化) |
| 113 | + |
| 114 | +初始化将所有的 $words[i]$ 存入 `Trie` 是必然的,我们只能考虑如何优化 `query` 操作。 |
| 115 | + |
| 116 | +在解法一中,我们需要对新数据流对应的字符串的每个后缀进行搜索,同时每次搜索是相互独立的,即本次匹配不会对下一次匹配产生贡献。 |
| 117 | + |
| 118 | +**实际上,我们可以通过「倒序建 `Trie`」的方式,将「枚举检查多个后缀」的操作变为「匹配一次后缀」操作。** |
| 119 | + |
| 120 | +具体的,我们可以在初始化 `StreamChecker` 时,将每个 $words[i]$ 翻转(倒序)加入 `Trie` 中;然后在 `query` 操作时(假设当前数据流对应的字符串为 `s`,长度为 $n$),从 `s` 的尾部开始在 `Trie` 中进行检索(即从 $s[n - 1]$ 开始往回找)。 |
| 121 | + |
| 122 | +若在某个位置 `idx` 时匹配成功,意味着 $s[idx ... (n-1)]$ 的翻转子串在字典树中,同时我们又是将每个 `words[i]` 进行倒序插入,即意味着 $s[idx ... (n - 1)]$ 的正向子串在 `words` 中,即满足 `s` 的某个后缀出现在 `words` 中。 |
| 123 | + |
| 124 | +同理,我们可以利用最大的 `words[i]` 长度为 200ドル$ 来控制从 $s[n - 1]$ 开始往回找的最远距离,同时利用当某个短后缀不在 `Trie` 中,则其余长度更大的后缀必然不在 `Trie` 中进行剪枝操作。 |
| 125 | + |
| 126 | +代码: |
| 127 | +```Java |
| 128 | +class StreamChecker { |
| 129 | + static int N = 2010 * 200, idx = 0; |
| 130 | + static int[][] tr = new int[N][26]; |
| 131 | + static boolean[] isEnd = new boolean[N * 26]; |
| 132 | + StringBuilder sb = new StringBuilder(); |
| 133 | + void add(String s) { |
| 134 | + int p = 0; |
| 135 | + for (int i = s.length() - 1; i >= 0; i--) { |
| 136 | + int u = s.charAt(i) - 'a'; |
| 137 | + if (tr[p][u] == 0) tr[p][u] = ++idx; |
| 138 | + p = tr[p][u]; |
| 139 | + } |
| 140 | + isEnd[p] = true; |
| 141 | + } |
| 142 | + public StreamChecker(String[] words) { |
| 143 | + for (int i = 0; i <= idx; i++) { |
| 144 | + Arrays.fill(tr[i], 0); |
| 145 | + isEnd[i] = false; |
| 146 | + } |
| 147 | + idx = 0; |
| 148 | + for (String s : words) add(s); |
| 149 | + } |
| 150 | + public boolean query(char c) { |
| 151 | + sb.append(c); |
| 152 | + int n = sb.length(), min = Math.max(0, n - 200), p = 0; |
| 153 | + for (int i = n - 1; i >= min; i--) { |
| 154 | + if (isEnd[p]) return true; |
| 155 | + int u = sb.charAt(i) - 'a'; |
| 156 | + if (tr[p][u] == 0) return false; |
| 157 | + p = tr[p][u]; |
| 158 | + } |
| 159 | + return isEnd[p]; |
| 160 | + } |
| 161 | +} |
| 162 | +``` |
| 163 | +* 时间复杂度:`StreamChecker` 初始化复杂度为 $O(n),ドル其中 $n$ 为 `words` 字符总数;`query` 操作复杂度为 $O(m),ドル其中 $m = 200$ 为最大 `words[i]` 长度 |
| 164 | +* 空间复杂度:$O(n \times C),ドル其中 $n$ 为 `words` 字符总数,$C = 26$ 为字符集大小 |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +### 最后 |
| 169 | + |
| 170 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1032` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 171 | + |
| 172 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 173 | + |
| 174 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 175 | + |
| 176 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 177 | + |
0 commit comments