LeetCode 466. 统计重复个数
题目描述


题意分析
源字符串是
s1连续重复n1次,目标的一组是s2连续重复n2次。允许从源字符串中删除字符,求最多能按顺序得到多少组完整目标;匹配的是子序列,字符不要求连续。
解法:循环节检测
核心思路
[!blue]
先不考虑
n2,连续匹配尽可能多份s2。用index表示下一次等待匹配的s2下标,遇到所需字符就立刻匹配;走完一份s2后令index = 0,完整匹配数matched加一。选择最早能匹配的位置不会减少后续可用空间,所以这种贪心能得到最多的完整份数。不实际展开重复字符串,而是逐份扫描
s1,用rounds记录已处理份数。每份s1都相同,因此在块边界上,从同一个index出发,之后的匹配过程和新增份数也完全相同;不断增长的matched不会影响下一步行为,状态只需index。用
seen[index]保存首次到达这个状态时的(rounds, matched)。再次遇到时,两次记录之差给出循环节:cycleRounds份s1能增加cycleMatches份s2,并回到相同index。剩余源串中还能容纳多少个完整循环,就同时增加对应的rounds和matched,index保持不变。跳过后剩余不足一个循环节,继续逐份扫描即可。代码用
accelerated保证只做一次批量跳跃。即使循环产出为 0,也可以照常跳过,因为跳跃次数只取决于消耗多少份s1。最终将完整s2份数除以n2,舍去不足一组的部分。
解题步骤
- 初始化
index = 0、rounds = 0、matched = 0,记录初始快照seen[0] = (0, 0)。- 扫描一份
s1,逐字符推进index,每匹配完整一份s2就重置下标并增加matched;随后令rounds++。- 未加速且当前
index首次出现时,保存快照;再次出现时,计算两次快照的轮数差和匹配数差。- 计算
cycles = (n1 - rounds) / cycleRounds,同时跳过对应源串份数并累加匹配产出,然后将accelerated置为true。- 扫描剩余源串,返回
matched / n2。
代码实现
class Solution {
public int getMaxRepetitions(String s1, int n1, String s2, int n2) {
Map<Integer, long[]> seen = new HashMap<>();
seen.put(0, new long[] {
0,
0
});
int index = 0;
int rounds = 0;
long matched = 0;
boolean accelerated = false;
while (rounds < n1) {
for (int i = 0; i < s1.length(); i++) {
if (s1.charAt(i) == s2.charAt(index)) {
index++;
if (index == s2.length()) {
index = 0;
matched++;
}
}
}
rounds++;
if (!accelerated) {
long[] previous = seen.get(index);
if (previous == null) {
seen.put(index, new long[] {
rounds,
matched
});
} else {
// 相同匹配下标之间的轮数差和产出差构成一个循环节。
int cycleRounds = rounds - (int) previous[0];
long cycleMatches = matched - previous[1];
// 只跳过剩余部分的完整循环,不足一轮的尾部继续扫描。
int cycles = (n1 - rounds) / cycleRounds;
rounds += cycles * cycleRounds;
matched += (long) cycles * cycleMatches;
accelerated = true;
}
}
}
// 累计量是单份目标词的个数,还需按每组所需份数折算。
return (int) (matched / n2);
}
}
type snapshot struct {
rounds int
matched int64
}
func getMaxRepetitions(s1 string, n1 int, s2 string, n2 int) int {
seen := map[int]snapshot{0: {}}
index, rounds := 0, 0
var matched int64
accelerated := false
for rounds < n1 {
for i := 0; i < len(s1); i++ {
if s1[i] == s2[index] {
index++
if index == len(s2) {
index = 0
matched++
}
}
}
rounds++
if !accelerated {
if previous, ok := seen[index]; ok {
// 相同匹配下标之间的轮数差和产出差构成一个循环节。
cycleRounds := rounds - previous.rounds
cycleMatches := matched - previous.matched
// 只跳过剩余部分的完整循环,不足一轮的尾部继续扫描。
cycles := (n1 - rounds) / cycleRounds
rounds += cycles * cycleRounds
matched += int64(cycles) * cycleMatches
accelerated = true
} else {
seen[index] = snapshot{rounds: rounds, matched: matched}
}
}
}
// 累计量是单份目标词的个数,还需按每组所需份数折算。
return int(matched / int64(n2))
}
复杂度分析
- 时间复杂度:$O(1+\min(n_1,\lvert s_2\rvert)\cdot\lvert s_1\rvert)$。块边界状态最多有 $\lvert s_2\rvert$ 种,发现循环前与跳过后的尾部各至多扫描这么多份
s1。- 空间复杂度:$O(1+\min(n_1,\lvert s_2\rvert))$,保存不同下标的快照。
关键点总结
[!green]
- 循环状态是匹配位置,不包含不断增加的累计产出。
- 跳跃同时增加轮数和匹配数,匹配位置保持不变。
- 不足完整循环的尾部仍需处理。
- 循环产出可以为零;跳跃次数按循环轮数计算,不对产出做除法。
易错点总结
[!yellow]
- 按连续子串匹配:子序列允许跳过无关字符。
- 循环长度直接取当前轮数:应减去前一次相同状态的轮数。
- 只更新轮数不更新产出:跳过的匹配结果丢失。
- 忘记除以 n2:返回的是 s2 份数。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 392. 判断子序列 | 简单 | 重复串匹配的基本步骤仍是子序列双指针,本题通过块边界状态循环跳过大量重复副本。 |
| 418. 屏幕可显示句子的数量 | 中等 | 同样反复处理固定块并检测状态周期,原题按屏幕行放词,本题按源串副本匹配目标。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!