|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1668. 最大重复子字符串](https://leetcode.cn/problems/maximum-repeating-substring/solution/by-ac_oier-xjhn/)** ,难度为 **简单**。 |
| 4 | + |
| 5 | +Tag : 「动态规划」、「序列 DP」、「字符串哈希」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个字符串 `sequence`,如果字符串 `word` 连续重复 `k` 次形成的字符串是 `sequence` 的一个子字符串,那么单词 `word` 的 重复值为 `k` 。 |
| 10 | + |
| 11 | +单词 `word` 的 最大重复值 是单词 `word` 在 `sequence` 中最大的重复值。如果 `word` 不是 `sequence` 的子串,那么重复值 `k` 为 `0` 。 |
| 12 | + |
| 13 | +给你一个字符串 `sequence` 和 `word` ,请你返回 最大重复值 `k` 。 |
| 14 | + |
| 15 | +示例 1: |
| 16 | +``` |
| 17 | +输入:sequence = "ababc", word = "ab" |
| 18 | +
|
| 19 | +输出:2 |
| 20 | +
|
| 21 | +解释:"abab" 是 "ababc" 的子字符串。 |
| 22 | +``` |
| 23 | +示例 2: |
| 24 | +``` |
| 25 | +输入:sequence = "ababc", word = "ba" |
| 26 | +
|
| 27 | +输出:1 |
| 28 | +
|
| 29 | +解释:"ba" 是 "ababc" 的子字符串,但 "baba" 不是 "ababc" 的子字符串。 |
| 30 | +``` |
| 31 | +示例 3: |
| 32 | +``` |
| 33 | +输入:sequence = "ababc", word = "ac" |
| 34 | +
|
| 35 | +输出:0 |
| 36 | +
|
| 37 | +解释:"ac" 不是 "ababc" 的子字符串。 |
| 38 | +``` |
| 39 | + |
| 40 | +提示: |
| 41 | +* $1 <= sequence.length <= 100$ |
| 42 | +* $1 <= word.length <= 100$ |
| 43 | +* `sequence` 和 `word` 都只包含小写英文字母。 |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +### 序列 DP |
| 48 | + |
| 49 | +为了方便,我们记 `sequence` 为 `ss`,记 `word` 为 `pp`,将两者长度分别记为 `n` 和 `m`。 |
| 50 | + |
| 51 | +同时我们调整「字符串」以及「将要用到的动规数组」的下标从 $1$ 开始。 |
| 52 | + |
| 53 | +这是一道入门级的「序列 DP」运用题,容易想到 **定义 $f[i]$ 为了考虑以 `ss[i]` 结尾时的最大重复值**。 |
| 54 | + |
| 55 | +不失一般性考虑 $f[i]$ 该如何转移:由于 `pp` 的长度已知,每次转移 $f[i]$ 时我们可以从 `ss` 中截取 **以 $ss[i]$ 为结尾,长度为 $m$ 的后缀字符串** `sub` 并与 `pp` 匹配,若两者相等,说明 `sub` 贡献了大小为 $1$ 的重复度,同时该重复度可累加在 $f[i - m]$ 上(好好回想我们的状态定义),即有状态转移方程:$f[i] = f[i - m] + 1$。 |
| 56 | + |
| 57 | +最终所有 $f[i]$ 的最大值即为答案。 |
| 58 | + |
| 59 | +Java 代码: |
| 60 | +```Java |
| 61 | +class Solution { |
| 62 | + public int maxRepeating(String ss, String pp) { |
| 63 | + int n = ss.length(), m = pp.length(), ans = 0; |
| 64 | + int[] f = new int[n + 10]; |
| 65 | + for (int i = 1; i <= n; i++) { |
| 66 | + if (i - m < 0) continue; |
| 67 | + if (ss.substring(i - m, i).equals(pp)) f[i] = f[i - m] + 1; |
| 68 | + ans = Math.max(ans, f[i]); |
| 69 | + } |
| 70 | + return ans; |
| 71 | + } |
| 72 | +} |
| 73 | +``` |
| 74 | +TypeScript 代码: |
| 75 | +```TypeScript |
| 76 | +function maxRepeating(ss: string, pp: string): number { |
| 77 | + let n = ss.length, m = pp.length, ans = 0 |
| 78 | + const f = new Array<number>(n + 10).fill(0) |
| 79 | + for (let i = 1; i <= n; i++) { |
| 80 | + if (i - m < 0) continue |
| 81 | + if (ss.substr(i - m, i) == pp) f[i] = f[i - m] + 1 |
| 82 | + ans = Math.max(ans, f[i]) |
| 83 | + } |
| 84 | + return ans |
| 85 | +} |
| 86 | +``` |
| 87 | +Python 代码: |
| 88 | +```Python |
| 89 | +class Solution: |
| 90 | + def maxRepeating(self, ss: str, pp: str) -> int: |
| 91 | + n, m, ans = len(ss), len(pp), 0 |
| 92 | + f = [0] * (n + 10) |
| 93 | + for i in range(1, n + 1): |
| 94 | + if i - m < 0: |
| 95 | + continue |
| 96 | + if ss[i - m:i] == pp: |
| 97 | + f[i] = f[i - m] + 1 |
| 98 | + ans = max(ans, f[i]) |
| 99 | + return ans |
| 100 | +``` |
| 101 | +* 时间复杂度:$O(n \times m)$ |
| 102 | +* 空间复杂度:$O(n)$ |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +### 字符串哈希 |
| 107 | + |
| 108 | +解法一的转移瓶颈在于:每次需要花费 $O(m)$ 的复杂度来生成子串,并进行字符串比较。 |
| 109 | + |
| 110 | +该过程可用「字符串哈希」进行优化:将 `ss` 和 `pp` 进行拼接得到完整字符串,并计算完整字符串的哈希数组和次方数组。随后从前往后检查 `ss`,若「某个以 $ss[i]$ 结尾长度为 `m` 的后缀字符串哈希值」与「 `pp` 字符串的哈希值」相等,说明找到了前驱状态值 $f[i - m]$,可进行转移。 |
| 111 | + |
| 112 | +我们通过 $O(n + m)$ 复杂度预处理了字符串哈希,将转移过程中「复杂度为 $O(m)$ 的子串截取与字符串比较」替换成了「复杂度为 $O(1)$ 的数值对比」,整体复杂度从 $O(n \times m)$ 下降到 $O(n + m)$。 |
| 113 | + |
| 114 | +> **不了解「字符串哈希」的同学可见前置 🧀 : [字符串哈希入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489813&idx=1&sn=7f3bc18ca390d85b17655f7164d8e660)。里面详解字符串哈希基本原理以及哈希冲突简单处理方式** |
| 115 | +
|
| 116 | +Java 代码: |
| 117 | +```Java |
| 118 | +class Solution { |
| 119 | + public int maxRepeating(String ss, String pp) { |
| 120 | + int n = ss.length(), m = pp.length(), ans = 0; |
| 121 | + int[] f = new int[n + 10]; |
| 122 | + |
| 123 | + String s = ss + pp; |
| 124 | + int P = 1313131, N = s.length(); |
| 125 | + long[] h = new long[N + 10], p = new long[N + 10]; |
| 126 | + p[0] = 1; |
| 127 | + for (int i = 1; i <= N; i++) { |
| 128 | + h[i] = h[i - 1] * P + s.charAt(i - 1); |
| 129 | + p[i] = p[i - 1] * P; |
| 130 | + } |
| 131 | + long phash = h[N] - h[N - m] * p[m]; |
| 132 | + |
| 133 | + for (int i = 1; i <= n; i++) { |
| 134 | + if (i - m < 0) continue; |
| 135 | + long cur = h[i] - h[i - m] * p[m]; |
| 136 | + if (cur == phash) f[i] = f[i - m] + 1; |
| 137 | + ans = Math.max(ans, f[i]); |
| 138 | + } |
| 139 | + return ans; |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | +Python 代码: |
| 144 | +```Python |
| 145 | +class Solution: |
| 146 | + def maxRepeating(self, ss: str, pp: str) -> int: |
| 147 | + n, m, ans = len(ss), len(pp), 0 |
| 148 | + f = [0] * (n + 10) |
| 149 | + |
| 150 | + s = ss + pp |
| 151 | + P, N, MOD = 131, len(s), 987654321 |
| 152 | + h, p = [0] * (N + 10), [0] * (N + 10) |
| 153 | + p[0] = 1 |
| 154 | + for i in range(1, N + 1): |
| 155 | + h[i] = (h[i - 1] * P + ord(s[i - 1])) % MOD |
| 156 | + p[i] = (p[i - 1] * P) % MOD |
| 157 | + phash = (h[N] - h[N - m] * p[m]) % MOD |
| 158 | + |
| 159 | + for i in range(1, n + 1): |
| 160 | + if i - m < 0: |
| 161 | + continue |
| 162 | + cur = (h[i] - h[i - m] * p[m]) % MOD |
| 163 | + if cur == phash: |
| 164 | + f[i] = f[i - m] + 1 |
| 165 | + ans = max(ans, f[i]) |
| 166 | + return ans |
| 167 | +``` |
| 168 | +* 时间复杂度:$O(n + m)$ |
| 169 | +* 空间复杂度:$O(n + m)$ |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +### 总结 |
| 174 | + |
| 175 | +这里简单说下「线性 DP」和「序列 DP」的区别。 |
| 176 | + |
| 177 | +线性 DP 通常强调「状态转移所依赖的前驱状态」是由给定数组所提供的,即拓扑序是由原数组直接给出。更大白话来说就是通常有 $f[i][...]$ 依赖于 $f[i - 1][...]$。 |
| 178 | + |
| 179 | +这就限定了线性 DP 的复杂度是简单由「状态数量(或者说是维度数)」所决定。 |
| 180 | + |
| 181 | +序列 DP 通常需要结合题意来寻找前驱状态,即需要自身寻找拓扑序关系(例如本题,需要自己结合题意来找到可转移的前驱状态 $f[i - m]$)。 |
| 182 | + |
| 183 | +这就限定了序列 DP 的复杂度是由「状态数 + 找前驱」的复杂度所共同决定。也直接导致了序列 DP 有很多玩法,往往能够结合其他知识点出题,来优化找前驱这一操作,通常是利用某些性质,或是利用数据结构进行优化。 |
| 184 | + |
| 185 | +--- |
| 186 | + |
| 187 | +### 最后 |
| 188 | + |
| 189 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1668` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 190 | + |
| 191 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 192 | + |
| 193 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 194 | + |
| 195 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 196 | + |
0 commit comments