【例题2】Power Strings(信息学奥赛一本通- P1466)
多组数据切忌滥用memset: 本题是多组测试数据,且最大长度达10^6。在KMP代码中,如果每读入一个字符串就,会引发海量的无用内存擦除,容易导致超时。因为pre()函数内部的for循环是严格基于索引逐个赋值的,会自动覆盖旧数据,因此无需清空。哈希匹配的内层break: 哈希校验时,一旦发现当前分块sum和基准块ans2不等,必须立刻将flag2置0并break出内层循环。若缺少此break,在
【题目描述】
原题来自:POJ 2406
给定若干个长度 ≤10^6 的字符串,询问每个字符串最多是由多少个相同的子字符串重复连接而成的。如:ababab 则最多有 3 个 ab 连接而成。
【输入】
输入若干行,每行有一个字符串,字符串仅含英语字母。特别的,字符串可能为 . 即一个半角句号,此时输入结束。
【输出】
【输入样例】
abcd
aaaa
ababab
.
【输出样例】
1
4
3
题目分析
本题是一道极其经典的字符串基础题。题目给定一个长度最大为10^6的字符串,要求找出它可以由多少个相同的子串重复拼接而成,求最大的重复次数。 将题意稍作转换:要求“最大的重复拼接次数”,等价于求字符串的“最短循环节”。找到了最短循环节,用总长度除以循环节的长度,即为最终答案。 针对这种循环节判断问题,竞赛中最常用的两套利器是字符串哈希与KMP算法的next数组。
思考过程与解题思路
面对10^6的数据级别,朴素的O(N^2)暴力截取比对必然会超时。我们必须将单次验证的时间复杂度降到极致。
思路一:字符串哈希(降维暴力) 既然不知道最短循环节是多长,最直观的思路就是“从小到大枚举”子串的长度 i。 只要原串长度len能被i整除,我们就把原串按长度i切成若干块,逐块比对。为了把字符串比较的O(K)时间降到O(1),我们引入前缀哈希。通过预处理前缀哈希数组,利用公式 Hash(L,R)=ans[R]−ans[L−1]×P[R−L+1],实现O(1)的切片校验。一旦某长度i校验通过,配合贪心思想,它必然是最短循环节。
思路二:KMP算法(核心定理推导) 如果说哈希是暴力的极致优化,KMP的next数组则是数学规律上的降维打击。 在0-base下标体系中,nxt[len-1]记录的是整个字符串的最长相等真前后缀长度。 这里存在一个极其优美的物理错位规律:将字符串的“前缀”与“后缀”对齐,它们之间在物理空间上错开的距离,正是字符串自我复制的最小周期。 因此得出核心定理:最小循环节长度=字符串总长度N-最长相等真前后缀nxt[N-1]。 最后仅需验证:若总长度能被该循环节整除,则完美循环;否则,该字符串只能由它本身构成(答案为1)。
算法设计与时空复杂度分析
1. 字符串哈希法
-
算法设计: 利用
unsigned long long自然溢出特性(隐式取模264)。采用1-base存储前缀哈希,极大地简化了区间查询时的越界判断。外层枚举循环节长度i,内层以步长i进行分块校验,遇到不匹配立即break剪枝。 -
时间复杂度: 预处理O(N)。校验阶段外层枚举因子,内层分块遍历。在加入了
ans2!=sum的极速break剪枝后,几乎不会出现极端的满载扫描,综合时间复杂度趋近于O(NlogN),实战运行极快。 -
空间复杂度:O(N),需维护
p数组和ans哈希数组。
2. KMP算法
-
算法设计: 0-base 标准模板求解
next数组。求出nxt[len-1]后,直接通过公式推导循环节,执行一次取模运算完成终极判断,逻辑呈线性。 -
时间复杂度: O(N),求解一次
next数组的开销。 -
空间复杂度: O(N),需维护
nxt数组。
易错点总结
-
多组数据切忌滥用
memset: 本题是多组测试数据,且最大长度达10^6。在KMP代码中,如果每读入一个字符串就memset(nxt,0,sizeof(nxt)),会引发海量的无用内存擦除,容易导致超时。因为pre()函数内部的for循环是严格基于索引逐个赋值的,会自动覆盖旧数据,因此无需清空。 -
哈希匹配的内层
break: 哈希校验时,一旦发现当前分块sum和基准块ans2不等,必须立刻将flag2置0并break出内层循环。若缺少此break,在遇到类似aaaaa...b这种只有末尾不同的极端构造数据时,程序将进行巨量的无效校验运算。 -
哈希数组预处理的越界防范: 字符串最大长度为1000000。预处理次幂数组
p[i]时,边界应稍大于此值(如1000005),以防最后边界访问越界导致RE。 -
KMP的下标极值: 在0-base体系中,长度为
len的字符串的最后一个字符下标永远是len-1。获取全串最长前后缀必须是nxt[len-1],切勿写成nxt[len]。
完整代码
方法一:字符串哈希(1-base)
//哈希做法
#include <iostream>
using namespace std;
typedef unsigned long long ull;
string s;
ull p[1000010];//存储131的i次方
ull ans[1000010];//存储字符串的前缀哈希(1-base)
//返回闭区间[l,r]的哈希值
ull gethash(int l,int r){
if(l>r) return 0;
return ans[r]-ans[l-1]*p[r-l+1];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
//预处理131的i次方
p[0]=1;
for(int i=1;i<=1000005;i++) p[i]=p[i-1]*131;
while(cin>>s){
if(s[0]=='.') return 0;
bool flag=0;//标记字符串s是否可以由比它短的子字符串重复连接而成
int len=s.size();
//1-base存储s的前缀哈希
for(int i=1;i<=len;i++) ans[i]=ans[i-1]*131+s[i-1];
//遍历可能的子字符串长度
for(int i=1;i<=len/2;i++){
//子串长度必须能被s整除
if(len%i!=0) continue;
bool flag2=1;//标记字符串s是否可以由长度为i的子字符串重复连接而成
//然后计算这一段长度的子串的哈希值
ull ans2=ans[i];
//然后去检查子串是否和s每个相同长度的部分哈希值都相等
for(int j=1;j<=len;j=j+i){
//求出s每一段和子串长度相同的哈希值
ull sum=gethash(j,j+i-1);
//如果不同 就标记flag2为0 并退出本轮查找
if(ans2!=sum){
flag2=0;
break;
}
}
//如果检查完成后 发现可以由i重复拼接而成
//就让标记flag为1 然后i为子串长度 输出len/i(多少个子串)
//然后就可以退出循环了
if(flag2==1){
flag=1;
cout<<len/i<<"\n";
break;
}
}
//如果不能由长度比s小的子串构成 就输出1(只能由自己组成)
if(flag==0) cout<<1<<"\n";
}
return 0;
}
方法二:KMP算法(0-base)
//kmp做法 最短循环节长度=字符串长度-最长公共前后缀
#include <iostream>
using namespace std;
string s;
int nxt[1000010];//存储s[0]-s[i]的最长真公共前后缀长度
//预处理字符串s的最长公共前后缀长度
void pre(){
int j=0;
int len=s.size();//字符串s长度
for(int i=1;i<len;i++){
while(j>0&&s[i]!=s[j]) j=nxt[j-1];
if(s[i]==s[j]) j++;
nxt[i]=j;
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
while(cin>>s){
//.即一个半角句号 此时输入结束
if(s[0]=='.') return 0;
int len=s.size();//字符串s长度
//处理字符串s的next数组
pre();
//最短循环节长度=字符串总长度-整个字符串的最长相等真前后缀长度
int len2=len-nxt[len-1];
//验证该循环节是否能完整铺满整个字符串
if(len%len2!=0) cout<<1<<"\n";//有残缺,无法构成循环
else cout<<len/len2<<"\n";//完整铺满,输出重复拼接的次数
}
return 0;
}
更多推荐

所有评论(0)