Skip to content

Commit 1d9071c

Browse files
committed
✨feat: add 1032
1 parent eaa546c commit 1d9071c

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

Index/字典树.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
| [677. 键值映射](https://leetcode-cn.com/problems/map-sum-pairs/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/map-sum-pairs/solution/gong-shui-san-xie-jie-he-dfs-de-trie-yun-i4xa/) | 中等 | 🤩🤩🤩🤩 |
1010
| [720. 词典中最长的单词](https://leetcode-cn.com/problems/longest-word-in-dictionary/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/longest-word-in-dictionary/solution/by-ac_oier-bmot/) | 简单 | 🤩🤩🤩🤩 |
1111
| [745. 前缀和后缀搜索](https://leetcode.cn/problems/prefix-and-suffix-search/) | [LeetCode 题解链接](https://leetcode.cn/problems/prefix-and-suffix-search/solution/by-ac_oier-ayej/) | 困难 | 🤩🤩🤩🤩 |
12+
| [1032. 字符流](https://leetcode.cn/problems/stream-of-characters/) | [LeetCode 题解链接](https://leetcode.cn/problems/stream-of-characters/solution/by-ac_oier-ihd4/) | 困难 | 🤩🤩🤩🤩🤩 |
1213
| [1707. 与数组中元素的最大异或值](https://leetcode-cn.com/problems/maximum-xor-with-an-element-from-array/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/maximum-xor-with-an-element-from-array/solution/gong-shui-san-xie-jie-zhe-ge-wen-ti-lai-lypqr/) | 困难 | 🤩🤩🤩 |
1314
| [剑指 Offer II 067. 最大的异或](https://leetcode.cn/problems/ms70jA/) | [LeetCode 题解链接](https://leetcode.cn/problems/ms70jA/solution/by-ac_oier-d9kx/) | 中等 | 🤩🤩🤩🤩 |
1415

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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

Comments
 (0)