目录

题目描述

466. 统计重复个数

题意分析

题目目标:定义 str1 为字符串 s1 连续重复 n1 次得到的串,str2 为 s2 连续重复 n2 次得到的串。要求找出最大的整数 m,使得「str2 重复 m 次」仍然能从 str1 中通过删除若干字符得到,也就是仍然是 str1 的子序列。
核心约束:注意匹配的是子序列而不是子串,允许跳过任意字符,这让匹配过程可以用一个简单的贪心双指针完成——从左到右扫描 str1,每遇到与当前待匹配字符相同的就前进一位。第二个约束是数据规模的极端不对称:s1 和 s2 的长度都不超过 100,但 n1 可以达到 $10^6$,这意味着 str1 的总长度可达 $10^8$,逐字符扫描一遍是可行的但已经很贴边,而如果 n1 再大就完全不行。这种「短模式 + 巨量重复」的形状是循环节检测的经典信号。第三个观察是最终答案要把匹配到的 s2 个数再除以 n2,因为一份 str2 由 n2 个 s2 拼成。
边界处理:s2 中可能存在 s1 里根本没有的字符,此时一个 s2 都匹配不出来,答案为 0;n1 可能只有 1,此时循环节还来不及形成就结束了,必须有一条不依赖循环节的兜底返回;循环节可能从第一轮就开始,也可能前面有一段不进入循环的前缀,两部分要分开结算;剩余轮数做整除和取模时,基准点必须与前缀的结束位置严格对齐,差一就会整体错位。

解法:循环节检测

核心思路

逐字符匹配 s2 时,贪心地遇到当前所需字符就匹配,能得到最多的完整 s2。把一整轮 s1 看成状态转移:进入时只需知道当前匹配到 s2 的下标 index,扫描后得到新的下标以及新增的完整匹配数。

index 只有 s2.length() 种取值。若某一轮结束后的 index 曾出现过,则两次快照之间的轮数和完整 s2 数构成循环节;从相同状态继续扫描,未来转移与产出必然重复。

哈希表记录 index -> (已处理 s1 轮数, 已匹配 s2 数)。发现循环后,将剩余轮数除以循环长度,一次跳过尽可能多的完整循环,再逐轮处理不足一个循环的尾部。

不变量是:rounds 表示已经完整扫描的 s1 份数,matched 表示这些字符中贪心匹配出的完整 s2 数,index 表示下一次要匹配的 s2 位置。循环跳跃从同一 index 出发并回到同一 index,所以不会改变后续尾部的匹配结果。最终 matched / n2 就是完整 str2 的最大重复数。

解题步骤

  1. 初始化 index = 0matched = 0rounds = 0,并记录初始快照。
  2. 每轮扫描一次 s1,按子序列规则推进 index;走完 s2 时归零并增加 matched
  3. 轮末若 index 首次出现,记录快照;若已出现,计算循环长度与循环产出。
  4. 跳过剩余轮数中的完整循环,再逐轮处理尾部。
  5. 返回 matched / n2

s1 = "acb", n1 = 4, s2 = "ab", n2 = 2 时,每轮都完成一个 s2 且回到 index = 0,循环长度为 1。四轮得到四个 s2,即两个 str2

s2 含有 s1 从未出现的字符,状态会以零产出形成循环并直接跳过,答案为 0。

代码实现

import java.util.HashMap;
import java.util.Map;

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(\min(n_1,\lvert s_2\rvert)\cdot\lvert s_1\rvert)$。循环前缀与跳跃后的尾部都受有限状态数限制。
  • 空间复杂度:$O(\min(n_1,\lvert s_2\rvert))$,用于保存不同 index 的快照。

关键点总结

  • 每轮 s1 是一个确定状态转移,完整状态只有 s2 的当前下标。
  • 相同下标再次出现,说明中间轮次与匹配产出会周期性重复。
  • 循环跳跃必须同时更新已处理轮数和完整匹配数。
  • 跳跃后仍需模拟不足一个循环的尾部。
  • 最终统计的是 s2 份数,还要除以 n2 才是 str2 份数。

易错点总结

  • 把子序列匹配写成连续子串匹配;"ab" 可以从 "acb" 中跳过 'c' 得到。
  • 将累计匹配数纳入循环状态,会因它单调增加而永远找不到重复状态。
  • 循环长度必须用两次快照的轮数差,循环产出必须用匹配数差。
  • 只跳循环而不处理尾部,会漏掉剩余不足一个循环的轮次。
  • 跳跃轮数计算若未扣除已处理的 rounds,会重复计算前缀。
  • 忘记最后除以 n2,返回的是 s2 数量而不是 str2 数量。

相似题目

题目 难度 考察点
392. 判断子序列 简单 本题内层匹配的最简形态,只判断一次是否为子序列,贪心双指针即可
792. 匹配子序列的单词数 中等 大量模式串对同一主串做子序列匹配,需要预处理位置表或分桶来避免重复扫描
459. 重复的子字符串 简单 同样围绕字符串的周期性,但目标是识别最小重复单元而非计数
202. 快乐数 简单 确定性状态转移下的循环检测,可用哈希记录或快慢指针,思路与本题同源
141. 环形链表 简单 循环检测的最纯粹形态,展示了用 $O(1)$ 空间替代快照数组的另一条路径
28. 找出字符串中第一个匹配项的下标 简单 换成子串匹配,考察 KMP 中前缀函数对周期结构的刻画