一、题目描述
题目定义
-
str = [s, n]:表示字符串s重复n次得到的新字符串(例如["abc", 3] = "abcabcabc")。 -
"字符串
A可以从字符串B获得":A是B的子序列 (即可以通过删除B中的某些字符得到A,不改变字符顺序)。
题目要求
给定字符串 s1、s2 和整数 n1、n2,构造两个字符串:
-
str1 = [s1, n1](s1重复n1次) -
str2 = [s2, n2](s2重复n2次)
请找到最大的整数 m ,使得 [str2, m](即 str2 重复 m 次)是 str1 的子序列。
二、示例
示例 1:
输入: s1 = "acb", n1 = 4, s2 = "ab", n2 = 2
**输出:**2
示例 2:
输入: s1 = "acb", n1 = 1, s2 = "acb", n2 = 1
**输出:**1
三、核心知识
-
**子序列匹配的基础逻辑:**子序列匹配是字符串处理的基础知识点,指在不改变字符相对顺序的前提下,通过遍历源字符串(s1),逐个匹配目标字符串(s2)的字符,若源字符串中能按顺序找到目标字符串的所有字符,则目标字符串是源字符串的子序列。本题中核心是单轮 s1 对 s2 的逐字符匹配逻辑,是后续优化的基础。
-
循环节(周期)检测与批量计算: 这是本题算法优化的核心知识点:由于 s2 的匹配位置(p2)仅有
len(s2)种有限状态,当相同 p2 状态重复出现时,说明匹配过程进入循环节。通过计算循环节内 "消耗的 s1 数量" 和 "匹配的 s2 数量",可批量计算剩余 s1 能匹配的 s2 次数,避免重复遍历,将时间复杂度从 O (n1len1) 降至 O (len1len2)。 -
**状态映射与记录:**利用数组 / 哈希表记录每个匹配状态(p2)首次出现时的累计值(已用 s1 数、已匹配 s2 数),是实现循环节检测的基础手段。通过状态与累计值的映射,能快速计算循环节的周期和贡献值,是连接 "单轮匹配" 和 "批量计算" 的关键。
-
**大数场景的非构造性优化:**面对 n1、n2 极大的场景,放弃直接构造超长字符串(会导致内存溢出 / 超时),转而通过 "计数替代构造" 的思路,仅记录匹配过程中的关键计数(cnt1、cnt2)和状态(p2),是处理大数量级字符串问题的核心优化思想。
-
**匹配指针的重置逻辑:**当 s2 的匹配指针(p2)遍历完整个 s2 时,重置指针并累加 s2 匹配次数(cnt2),这是统计 s2 总匹配次数的基础操作,确保能持续、准确地计数多轮 s2 的匹配结果。
四、代码实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int getMaxRepetitions(char * s1, int n1, char * s2, int n2) {
int len1 = strlen(s1);
int len2 = strlen(s2);
// 特殊情况:s2为空,直接返回0
if (len2 == 0) {
return 0;
}
// 记录已使用的s1个数、已匹配的s2个数、当前匹配到s2的位置
int cnt1 = 0;
int cnt2 = 0;
int p2 = 0;
// pos_map[p]:记录p2=p时,第一次出现的cnt1和cnt2(避免重复计算)
// 因为p2的范围是0~len2-1,所以数组大小为len2即可
int pos_map[101][2] = {0}; // pos_map[p][0] = cnt1, pos_map[p][1] = cnt2
memset(pos_map, -1, sizeof(pos_map)); // 初始化为-1,表示未访问
while (cnt1 < n1) {
// 检测循环节:当前p2已经出现过
if (pos_map[p2][0] != -1) {
// 之前出现时的cnt1和cnt2
int pre_cnt1 = pos_map[p2][0];
int pre_cnt2 = pos_map[p2][1];
// 一个循环节内的s1数量和s2数量
int cycle_cnt1 = cnt1 - pre_cnt1;
int cycle_cnt2 = cnt2 - pre_cnt2;
// 剩余可用的s1数量
int remain_cnt1 = n1 - cnt1;
// 可以完整循环的次数
int cycle_num = remain_cnt1 / cycle_cnt1;
// 批量累加cnt2和cnt1
cnt2 += cycle_num * cycle_cnt2;
cnt1 += cycle_num * cycle_cnt1;
// 重置pos_map,避免再次进入循环(已处理完所有周期)
memset(pos_map, -1, sizeof(pos_map));
continue;
}
// 记录当前p2的位置对应的cnt1和cnt2
pos_map[p2][0] = cnt1;
pos_map[p2][1] = cnt2;
// 用当前s1匹配s2
for (int i = 0; i < len1; i++) {
if (s1[i] == s2[p2]) {
p2++;
// 完成一个s2的匹配
if (p2 == len2) {
cnt2++;
p2 = 0;
}
}
}
// 用完了一个s1
cnt1++;
}
// 最大的m是cnt2 // n2
return cnt2 / n2;
}
五、代码逐段解析
1. 特殊情况处理
if (len2 == 0) {
return 0;
}
如果 s2 为空,直接返回 0(空字符串是任何字符串的子序列,但题目中 s2 长度至少为 1)。
2. 变量初始化
int cnt1 = 0; // 已使用的s1个数
int cnt2 = 0; // 已匹配的s2个数
int p2 = 0; // 当前匹配到s2的位置(0~len2-1)
int pos_map[101][2] = {0};
memset(pos_map, -1, sizeof(pos_map));
pos_map用于记录每个 p2 第一次出现时的 cnt1 和 cnt2,初始化为 -1 表示未访问
3. 循环匹配与循环节检测
while (cnt1 < n1) {
// 检测循环节
if (pos_map[p2][0] != -1) {
int pre_cnt1 = pos_map[p2][0];
int pre_cnt2 = pos_map[p2][1];
int cycle_cnt1 = cnt1 - pre_cnt1;
int cycle_cnt2 = cnt2 - pre_cnt2;
int remain_cnt1 = n1 - cnt1;
int cycle_num = remain_cnt1 / cycle_cnt1;
cnt2 += cycle_num * cycle_cnt2;
cnt1 += cycle_num * cycle_cnt1;
memset(pos_map, -1, sizeof(pos_map));
continue;
}
// 记录当前状态
pos_map[p2][0] = cnt1;
pos_map[p2][1] = cnt2;
// 用s1匹配s2
for (int i = 0; i < len1; i++) {
if (s1[i] == s2[p2]) {
p2++;
if (p2 == len2) {
cnt2++;
p2 = 0;
}
}
}
cnt1++;
}
-
循环节检测 :当
p2重复出现时,计算循环节的周期(cycle_cnt1是一个周期用的s1数,cycle_cnt2是一个周期匹配的s2数),然后批量计算剩余s1能匹配的次数,避免重复遍历。 -
子序列匹配 :遍历
s1的每个字符,若与s2[p2]匹配,则p2后移;若p2到达s2末尾,说明完成一个s2的匹配,cnt2加 1 并重置p2。
4. 计算结果
return cnt2 / n2;
因为 [str2, m] 是 s2 的 n2*m 次重复,所以最大的 m 是 cnt2 // n2(cnt2 是 s2 的总匹配次数)。
六、复杂度分析
-
时间复杂度 :
O(len1 * len2)由于p2的状态最多有len2种(0~len2-1),所以循环最多执行len2次,每次遍历s1的len1个字符,因此时间复杂度是O(len1 * len2)。对于len1、len2 ≤ 100的限制,该复杂度非常高效。 -
空间复杂度 :
O(len2)仅用了大小为len2的数组pos_map,空间开销可忽略。
七、解题关键
-
理解 "
str2的m次重复" 与 "s2的n2*m次重复" 的等价性; -
识别匹配状态的循环性,利用循环节减少重复计算;
-
正确处理子序列匹配的边界条件(如
p2重置)。