Skip to content

Commit 6307d71

Browse files
committed
✨feat: Add 686
1 parent 4048d2b commit 6307d71

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed

Index/子串匹配.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
| 题目 | 题解 | 难度 | 推荐指数 |
22
| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | -------- |
33
| [28. 实现 strStr()](https://leetcode-cn.com/problems/implement-strstr/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/implement-strstr/solution/shua-chuan-lc-shuang-bai-po-su-jie-fa-km-tb86/) | 简单 | 🤩🤩🤩🤩🤩 |
4+
| [686. 重复叠加字符串匹配](https://leetcode-cn.com/problems/repeated-string-match/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/repeated-string-match/solution/gong-shui-san-xie-yi-ti-san-jie-qia-chan-3hbr/) | 中等 | 🤩🤩🤩🤩 |
45

Index/字符串哈希.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
| 题目 | 题解 | 难度 | 推荐指数 |
22
| ------------------------------------------------------------ | ------------------------------------------------------------ | ---- | -------- |
33
| [187. 重复的DNA序列](https://leetcode-cn.com/problems/repeated-dna-sequences/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/repeated-dna-sequences/solution/gong-shui-san-xie-yi-ti-shuang-jie-hua-d-30pg/) | 中等 | 🤩🤩🤩🤩 |
4+
| [686. 重复叠加字符串匹配](https://leetcode-cn.com/problems/repeated-string-match/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/repeated-string-match/solution/gong-shui-san-xie-yi-ti-san-jie-qia-chan-3hbr/) | 中等 | 🤩🤩🤩🤩 |
45

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
### 题目描述
2+
3+
这是 LeetCode 上的 **[686. 重复叠加字符串匹配](https://leetcode-cn.com/problems/repeated-string-match/solution/gong-shui-san-xie-yi-ti-san-jie-qia-chan-3hbr/)** ,难度为 **中等**
4+
5+
Tag : 「字符串哈希」、「KMP」
6+
7+
8+
9+
给定两个字符串 `a``b`,寻找重复叠加字符串 `a` 的最小次数,使得字符串 `b` 成为叠加后的字符串 `a` 的子串,如果不存在则返回 `-1`
10+
11+
注意:字符串 `"abc"` 重复叠加 `0` 次是 `""`,重复叠加 `1` 次是 `"abc"`,重复叠加 `2` 次是 `"abcabc"`
12+
13+
示例 1:
14+
```
15+
输入:a = "abcd", b = "cdabcdab"
16+
17+
输出:3
18+
19+
解释:a 重复叠加三遍后为 "abcdabcdabcd", 此时 b 是其子串。
20+
```
21+
示例 2:
22+
```
23+
输入:a = "a", b = "aa"
24+
25+
输出:2
26+
```
27+
示例 3:
28+
```
29+
输入:a = "a", b = "a"
30+
31+
输出:1
32+
```
33+
示例 4:
34+
```
35+
输入:a = "abc", b = "wxyz"
36+
37+
输出:-1
38+
```
39+
40+
提示:
41+
* $1 <= a.length <= 10^4$
42+
* $1 <= b.length <= 10^4$
43+
* `a``b` 由小写英文字母组成
44+
45+
---
46+
47+
### 基本分析
48+
49+
首先,可以分析复制次数的「下界」和「上界」为何值:
50+
51+
**对于「下界」的分析是容易的:至少将 `a` 复制长度大于等于 `b` 的长度,才有可能匹配。**
52+
53+
在明确了「下界」后,再分析再经过多少次复制,能够明确得到答案,能够得到明确答案的最小复制次数即是上界。
54+
55+
**由于主串是由 `a` 复制多次而来,并且是从主串中找到子串 `b`,因此可以明确子串的起始位置,不会超过 `a` 的长度。**
56+
57+
![image.png](https://pic.leetcode-cn.com/1640128316-fdWyyB-image.png)
58+
59+
**长度越过 `a` 长度的起始匹配位置,必然在此前已经被匹配过了。**
60+
61+
由此,我们可知复制次数「上界」最多为「下界 + $1$」。
62+
63+
`a` 的长度为 $n$,`b` 的长度为 $m$,下界次数为 $c1$,上界次数为 $c2 = c1 + 1$。
64+
65+
因此我们可以对 `a` 复制 $c2$ 次,得到主串后匹配 `b`,如果匹配成功后的结束位置不超过了 $n * c1$,说明复制 $c1$ 即可,返回 $c1$,超过则返回 $c2$;匹配不成功则返回 $-1$。
66+
67+
---
68+
69+
### 卡常
70+
71+
这是我最开始的 AC 版本。
72+
73+
虽然这是道挺显然的子串匹配问题,但是昨晚比平时晚睡了一个多小时,早上起来精神状态不是很好,身体的每个细胞都在拒绝写 KMP 🤣
74+
75+
就动了歪脑筋写了个「卡常」做法。
76+
77+
通过该做法再次印证了 LC 的评测机制十分奇葩:居然不是对每个用例单独计时,也不是算总的用例用时,而是既算单用例耗时,又算总用时??
78+
79+
导致我直接 `TLE` 了 $6$ 次才通过(从 $700$ 试到了 $100$),其中有 $4$ 次 `TLE` 是显示通过了所有样例,但仍然 `TLE`,我不理解为什么要设置这样迷惑的机制。
80+
81+
回到该做法本身,首先对 `a` 进行复制确保长度大于等于 `b`,然后在一定时间内,不断的「复制 - 检查」,如果在规定时间内能够找到则返回复制次数,否则返回 `-1`
82+
83+
代码:
84+
```Java
85+
import java.time.Clock;
86+
class Solution {
87+
public int repeatedStringMatch(String a, String b) {
88+
StringBuilder sb = new StringBuilder();
89+
int ans = 0;
90+
while (sb.length() < b.length() && ++ans > 0) sb.append(a);
91+
Clock clock = Clock.systemDefaultZone();
92+
long start = clock.millis();
93+
while (clock.millis() - start < 100) {
94+
if (sb.indexOf(b) != -1) return ans;
95+
sb.append(a);
96+
ans++;
97+
}
98+
return -1;
99+
}
100+
}
101+
```
102+
* 时间复杂度:$O(C)$
103+
* 空间复杂度:$O(C)$
104+
105+
---
106+
107+
### 上下界性质
108+
109+
通过「基本分析」后,我们发现「上下界」具有准确的大小关系,其实不需要用到「卡常」做法。
110+
111+
只需要进行「上界」次复制后,尝试匹配,根据匹配结果返回答案即可。
112+
113+
代码:
114+
```Java
115+
class Solution {
116+
public int repeatedStringMatch(String a, String b) {
117+
StringBuilder sb = new StringBuilder();
118+
int ans = 0;
119+
while (sb.length() < b.length() && ++ans > 0) sb.append(a);
120+
sb.append(a);
121+
int idx = sb.indexOf(b);
122+
if (idx == -1) return -1;
123+
return idx + b.length() > a.length() * ans ? ans + 1 : ans;
124+
}
125+
}
126+
```
127+
* 时间复杂度:需要 $\left \lceil \frac{m}{n} \right \rceil + 1$ 次拷贝 和 一次子串匹配。复杂度为 $O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
128+
* 空间复杂度:$O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
129+
130+
---
131+
132+
### KMP
133+
134+
其中 `indexOf` 部分可以通过 KMP/字符串哈希 实现,不熟悉 KMP 的同学,可以查看 [一文详解 KMP 算法](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486317&idx=1&sn=9c2ff2fa5db427133cce9c875064e7a4&chksm=fd9ca072caeb29642bf1f5c151e4d5aaff4dc10ba408b23222ea1672cfc41204a584fede5c05&token=1782709324&lang=zh_CN#rd),里面通过大量配图讲解了 KMP 的匹配过程与提供了实用模板。
135+
136+
使用 KMP 代替 `indexOf` 可以有效利用主串是由多个 `a` 复制而来的性质。
137+
138+
代码:
139+
```Java
140+
class Solution {
141+
public int repeatedStringMatch(String a, String b) {
142+
StringBuilder sb = new StringBuilder();
143+
int ans = 0;
144+
while (sb.length() < b.length() && ++ans > 0) sb.append(a);
145+
sb.append(a);
146+
int idx = strStr(sb.toString(), b);
147+
if (idx == -1) return -1;
148+
return idx + b.length() > a.length() * ans ? ans + 1 : ans;
149+
}
150+
151+
int strStr(String ss, String pp) {
152+
if (pp.isEmpty()) return 0;
153+
154+
// 分别读取原串和匹配串的长度
155+
int n = ss.length(), m = pp.length();
156+
// 原串和匹配串前面都加空格,使其下标从 1 开始
157+
ss = " " + ss;
158+
pp = " " + pp;
159+
160+
char[] s = ss.toCharArray();
161+
char[] p = pp.toCharArray();
162+
163+
// 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相关的)
164+
int[] next = new int[m + 1];
165+
// 构造过程 i = 2,j = 0 开始,i 小于等于匹配串长度 【构造 i 从 2 开始】
166+
for (int i = 2, j = 0; i <= m; i++) {
167+
// 匹配不成功的话,j = next(j)
168+
while (j > 0 && p[i] != p[j + 1]) j = next[j];
169+
// 匹配成功的话,先让 j++
170+
if (p[i] == p[j + 1]) j++;
171+
// 更新 next[i],结束本次循环,i++
172+
next[i] = j;
173+
}
174+
175+
// 匹配过程,i = 1,j = 0 开始,i 小于等于原串长度 【匹配 i 从 1 开始】
176+
for (int i = 1, j = 0; i <= n; i++) {
177+
// 匹配不成功 j = next(j)
178+
while (j > 0 && s[i] != p[j + 1]) j = next[j];
179+
// 匹配成功的话,先让 j++,结束本次循环后 i++
180+
if (s[i] == p[j + 1]) j++;
181+
// 整一段匹配成功,直接返回下标
182+
if (j == m) return i - m;
183+
}
184+
return -1;
185+
}
186+
}
187+
```
188+
* 时间复杂度:需要 $\left \lceil \frac{m}{n} \right \rceil + 1$ 次拷贝 和 一次子串匹配。复杂度为 $O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
189+
* 空间复杂度:$O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
190+
191+
---
192+
193+
### 字符串哈希
194+
195+
结合「基本分析」,我们知道这本质是一个子串匹配问题,我们可以使用「字符串哈希」来解决。
196+
197+
`a` 的长度为 $n$,`b` 的长度为 $m$。
198+
199+
仍然是先将 `a` 复制「上界」次,得到主串 `ss`,目的是从 `ss` 中检测是否存在子串为 `b`
200+
201+
在字符串哈希中,为了方便,我们将 `ss``b` 进行拼接,设拼接后长度为 $len$,那么 `b` 串的哈希值为 $[len - m + 1, len]$ 部分(下标从 $1$ 开始),记为 $target$。
202+
203+
然后在 $[1, n]$ 范围内枚举起点,尝试找长度为 $m$ 的哈希值与 $target$ 相同的哈希值。
204+
205+
代码:
206+
```Java
207+
class Solution {
208+
public int repeatedStringMatch(String a, String b) {
209+
StringBuilder sb = new StringBuilder();
210+
int ans = 0;
211+
while (sb.length() < b.length() && ++ans > 0) sb.append(a);
212+
sb.append(a);
213+
int idx = strHash(sb.toString(), b);
214+
if (idx == -1) return -1;
215+
return idx + b.length() > a.length() * ans ? ans + 1 : ans;
216+
}
217+
int strHash(String ss, String b) {
218+
int P = 131;
219+
int n = ss.length(), m = b.length();
220+
String str = ss + b;
221+
int len = str.length();
222+
int[] h = new int[len + 10], p = new int[len + 10];
223+
h[0] = 0; p[0] = 1;
224+
for (int i = 0; i < len; i++) {
225+
p[i + 1] = p[i] * P;
226+
h[i + 1] = h[i] * P + str.charAt(i);
227+
}
228+
int r = len, l = r - m + 1;
229+
int target = h[r] - h[l - 1] * p[r - l + 1]; // b 的哈希值
230+
for (int i = 1; i <= n; i++) {
231+
int j = i + m - 1;
232+
int cur = h[j] - h[i - 1] * p[j - i + 1]; // 子串哈希值
233+
if (cur == target) return i - 1;
234+
}
235+
return -1;
236+
}
237+
}
238+
```
239+
* 时间复杂度:需要 $\left \lceil \frac{m}{n} \right \rceil + 1$ 次拷贝 和 一次子串匹配。复杂度为 $O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
240+
* 空间复杂度:$O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
241+
242+
---
243+
244+
### 最后
245+
246+
这是我们「刷穿 LeetCode」系列文章的第 `No.686` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
247+
248+
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
249+
250+
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。
251+
252+
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
253+

0 commit comments

Comments
 (0)