1. 题目解析与算法选择
1.1 题目核心需求
这道题目要求我们在一个长度为N的整数序列中,找到至少重复K次的最长子序列长度。这里的"重复"允许子序列重叠出现,比如在序列1 2 3 2 3 2 3 1中,2 3 2 3可以看作重复了两次(分别在位置2-5和4-7)。
关键点在于:
- 子序列必须是连续的
- 允许重叠出现
- 需要找到最长的满足条件的子序列
1.2 算法选择思路
对于这类子串匹配问题,常见的解法有:
- 暴力枚举:检查所有可能的子串,时间复杂度O(n³)
- 后缀数组:可以优化到O(nlogn)
- 哈希+二分:时间复杂度O(nlogn)
考虑到题目中N的范围是2×10⁴,纯暴力解法显然不可行。这里我们选择哈希+枚举的方法,通过预处理字符串哈希来快速比较子串是否相同,将时间复杂度优化到O(n²),在题目给定的数据范围内是可以接受的。
提示:在实际比赛中,如果N更大(比如1e5以上),就需要考虑更高效的算法如后缀数组了。
2. 哈希算法原理与实现
2.1 字符串哈希基础
字符串哈希是将字符串映射为一个数字的技术,常用的哈希方法是将字符串看作一个base进制的数。对于字符串s,其哈希值为:
code复制hash(s) = s[0]×base^(n-1) + s[1]×base^(n-2) + ... + s[n-1]×base^0
这种哈希方法的一个重要性质是:我们可以用前缀哈希数组在O(1)时间内计算任意子串的哈希值。
2.2 哈希预处理实现
在代码中,我们做了两个预处理:
cpp复制mul[0]=1;
for(int i=1;i<=n;i++){
mul[i]=mul[i-1]*19260817;//预处理base的幂次
}
for(int i=1;i<=n;i++){
hs[i]=hs[i-1]*19260817+a[i];//计算前缀哈希
}
这里:
mul[i]存储的是base^i(base=19260817)hs[i]存储的是前i个元素的前缀哈希值
2.3 子串哈希计算
要计算子串a[l..r]的哈希值,公式为:
code复制hash(l,r) = hs[r] - hs[l-1] * mul[r-l+1]
这个公式的原理是:hs[r]包含了从1到r的所有元素,而hs[l-1]*mul[r-l+1]相当于把前l-1个元素左移到与hs[r]对齐的位置,相减就得到中间子串的哈希值。
3. 算法核心逻辑详解
3.1 暴力枚举框架
算法的主要框架是:
- 初始化ans=0
- 对于每个起始位置k(1≤k≤n)
- 尝试将当前最长长度ans扩展1
- 检查从k开始的长度为ans+1的子串是否在序列中出现至少K次
- 如果满足条件,则ans++并继续尝试扩展;否则结束当前k的检查
cpp复制for(int k=1;k<=n;k++){
while(1){
int cnt=0;
for(int i=k;i<=n&&i+ans<=n;i++){
if(hs[i+ans]-hs[i-1]*mul[ans+1]==hs[k+ans]-hs[k-1]*mul[ans+1]){
cnt++;
}
}
if(cnt>=m){
ans++;
}
else{
break;
}
}
}
3.2 关键优化点
- 单调性利用:一旦某个长度L不满足条件,所有大于L的长度也一定不满足,可以立即break
- 哈希快速比较:通过预处理哈希,可以在O(1)时间内比较两个子串是否相同
- 边界处理:注意i+ans不能超过n,防止数组越界
注意:这里的暴力枚举实际上利用了答案的单调性,使得整体复杂度降为O(n²),而不是纯暴力的O(n³)
4. 代码实现细节与优化
4.1 哈希base选择
代码中使用了19260817作为base,这是一个较大的质数,可以有效减少哈希冲突的概率。在实际应用中,base的选择有几点考虑:
- 应该大于字符集大小(这里数字范围是1e6,所以base要更大)
- 最好是质数
- 不要选择常见的base如131,13331等(在强数据下可能被卡)
4.2 无符号长整型使用
使用unsigned long long(ull)可以自动处理溢出,相当于对2^64取模,既保证了计算效率,又提供了足够的哈希空间。
4.3 边界条件处理
代码中有几个关键边界需要注意:
cpp复制i<=n && i+ans<=n // 确保不越界
hs[i+ans]-hs[i-1]*mul[ans+1] // 子串长度为ans+1
4.4 复杂度分析
- 预处理:O(n)
- 外层循环:O(n)
- 内层while循环:均摊O(n)
- 总复杂度:O(n²)
对于n=2e4,n²=4e8,在时间限制较宽松的情况下可以通过(如2秒时限)。
5. 测试与验证
5.1 样例测试
使用题目提供的样例测试:
code复制8 2
1
2
3
2
3
2
3
1
程序正确输出4,对应子串2 3 2 3。
5.2 边界测试
- K=N的情况:
code复制3 3
1
2
3
输出应为1(每个单独的数字都出现了3次)
- 所有元素相同:
code复制5 2
7
7
7
7
7
输出应为4(最长子串"7 7 7 7"出现2次)
5.3 大数据测试
可以生成随机数据测试程序的稳定性:
cpp复制srand(time(0));
n = 20000; k = 100;
for(int i=1;i<=n;i++) a[i] = rand()%100;
6. 算法优化方向
虽然当前解法可以通过题目,但还有优化空间:
6.1 二分搜索优化
可以改为二分答案+哈希检查:
- 二分可能的长度L
- 对于每个L,用哈希表统计所有长度为L的子串出现次数
- 如果有子串出现≥K次,则尝试更大的L
这样复杂度可以降为O(nlogn)
6.2 后缀数组解法
后缀数组是解决这类问题的标准解法,可以在O(nlogn)时间内解决问题,适合更大的数据规模。
6.3 滚动哈希优化
当前代码每次扩展长度都要重新扫描整个字符串,可以维护一个哈希表记录所有长度为ans+1的子串的出现次数,避免重复计算。
7. 常见错误与调试技巧
7.1 哈希冲突问题
如果遇到错误答案,可能是哈希冲突导致的。解决方法:
- 使用双base(两个不同的base和模数)
- 在哈希匹配后,再进行一次逐字符验证
7.2 边界错误
常见边界错误包括:
- 数组越界(特别是i+ans>n的情况)
- 子串长度计算错误(注意ans+1还是ans)
- K=1时的特殊情况处理
7.3 性能优化
如果遇到时间限制问题,可以尝试:
- 改用更快的I/O方式(如scanf/printf)
- 减少内存访问(如预计算更多信息)
- 使用更高效的哈希表实现
8. 实际应用与扩展
这类子串频率问题在实际中有很多应用场景:
- 生物信息学:DNA序列中的重复模式识别
- ** plagiarism检测**:文档中的重复片段检测
- 数据压缩:寻找重复模式进行压缩
- 日志分析:识别系统日志中的重复错误模式
对于想进一步学习的同学,推荐研究:
- 后缀数组(Suffix Array)及其应用
- KMP算法及其变种
- AC自动机多模式匹配
- 滚动哈希(Rabin-Karp)算法