LeetCode 466:统计重复个数

一、题目描述

题目定义

  • str = [s, n]:表示字符串 s 重复 n 次得到的新字符串(例如 ["abc", 3] = "abcabcabc")。

  • "字符串 A 可以从字符串 B 获得":AB子序列 (即可以通过删除 B 中的某些字符得到 A,不改变字符顺序)。

题目要求

给定字符串 s1s2 和整数 n1n2,构造两个字符串:

  • 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

三、核心知识

  1. **子序列匹配的基础逻辑:**子序列匹配是字符串处理的基础知识点,指在不改变字符相对顺序的前提下,通过遍历源字符串(s1),逐个匹配目标字符串(s2)的字符,若源字符串中能按顺序找到目标字符串的所有字符,则目标字符串是源字符串的子序列。本题中核心是单轮 s1 对 s2 的逐字符匹配逻辑,是后续优化的基础。

  2. 循环节(周期)检测与批量计算: 这是本题算法优化的核心知识点:由于 s2 的匹配位置(p2)仅有len(s2)种有限状态,当相同 p2 状态重复出现时,说明匹配过程进入循环节。通过计算循环节内 "消耗的 s1 数量" 和 "匹配的 s2 数量",可批量计算剩余 s1 能匹配的 s2 次数,避免重复遍历,将时间复杂度从 O (n1len1) 降至 O (len1len2)。

  3. **状态映射与记录:**利用数组 / 哈希表记录每个匹配状态(p2)首次出现时的累计值(已用 s1 数、已匹配 s2 数),是实现循环节检测的基础手段。通过状态与累计值的映射,能快速计算循环节的周期和贡献值,是连接 "单轮匹配" 和 "批量计算" 的关键。

  4. **大数场景的非构造性优化:**面对 n1、n2 极大的场景,放弃直接构造超长字符串(会导致内存溢出 / 超时),转而通过 "计数替代构造" 的思路,仅记录匹配过程中的关键计数(cnt1、cnt2)和状态(p2),是处理大数量级字符串问题的核心优化思想。

  5. **匹配指针的重置逻辑:**当 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 第一次出现时的 cnt1cnt2,初始化为 -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]s2n2*m 次重复,所以最大的 mcnt2 // n2cnt2s2 的总匹配次数)。

六、复杂度分析

  • 时间复杂度O(len1 * len2)由于 p2 的状态最多有 len2 种(0~len2-1),所以循环最多执行 len2 次,每次遍历 s1len1 个字符,因此时间复杂度是 O(len1 * len2)。对于 len1len2 ≤ 100 的限制,该复杂度非常高效。

  • 空间复杂度O(len2)仅用了大小为 len2 的数组 pos_map,空间开销可忽略。

七、解题关键

  1. 理解 "str2m 次重复" 与 "s2n2*m 次重复" 的等价性;

  2. 识别匹配状态的循环性,利用循环节减少重复计算;

  3. 正确处理子序列匹配的边界条件(如 p2 重置)。

相关推荐
TYFHVB122 小时前
2026六大主流CRM横评,五大核心维度深度解析
大数据·前端·数据结构·人工智能
爱和冰阔落2 小时前
【C++STL上】栈和队列模拟实现 容器适配器 力扣经典算法秘籍
数据结构·c++·算法·leetcode·广度优先
程序员-King.2 小时前
day162—递归—买卖股票的最佳时机Ⅱ(LeetCode-122)
算法·leetcode·深度优先·递归
Gorgous—l2 小时前
数据结构算法学习:LeetCode热题100-贪心算法篇(数组中的第K个最大元素、 前 K 个高频元素、数据流的中位数)
数据结构·学习·算法
一叶落4382 小时前
LeetCode 300. 最长递增子序列(LIS)详解(C语言 | DP + 二分优化)
c语言·数据结构·c++·算法·leetcode
Darkwanderor2 小时前
数据结构——trie(字典)树
数据结构·c++·字典树·trie树
灰色小旋风2 小时前
力扣第11题C++盛最多水的容器
数据结构·算法·leetcode
一匹电信狗2 小时前
【LeetCode面试题17.04】消失的数字
c语言·开发语言·数据结构·c++·算法·leetcode·stl
j_xxx404_2 小时前
从 O(N) 到 O(log N):LCR 173 点名问题的五种解法与最优推导
开发语言·c++·算法