【题目描述】
Zxl有一次决定制造一条项链,她以非常便宜的价格买了一长条鲜艳的珊瑚珠子,她现在也有一个机器,能把这条珠子切成很多块(子串),每块有k(k>0)个珠子,如果这条珠子的长度不是k的倍数,最后一块小于k的就不要拉(nc真浪费),保证珠子的长度为正整数。 Zxl喜欢多样的项链,为她应该怎样选择数字k来尽可能得到更多的不同的子串感到好奇,子串都是可以反转的,换句话说,子串(1,2,3)和(3,2,1)是一样的。写一个程序,为Zxl决定最适合的k从而获得最多不同的子串。
例如:这一串珠子是: (1,1,1,2,2,2,3,3,3,1,2,3,3,1,2,2,1,3,3,2,1)。
k=1的时候,我们得到3个不同的子串:(1),(2),(3)
k=2的时候,我们得到6个不同的子串: (1,1),(1,2),(2,2),(3,3),(3,1),(2,3)
k=3的时候,我们得到5个不同的子串: (1,1,1),(2,2,2),(3,3,3),(1,2,3),(3,1,2)
k=4的时候,我们得到5个不同的子串: (1,1,1,2),(2,2,3,3),(3,1,2,3),(3,1,2,2),(1,3,3,2)
【输入】
共有两行,第一行一个整数n代表珠子的长度,(n≤200000),第二行是由空格分开的颜色ai(1≤ai≤n)。
【输出】
也有两行,第一行两个整数,第一个整数代表能获得的最大不同的子串个数,第二个整数代表能获得最大值的k的个数,第二行输出所有的k(中间有空格)。
【输入样例】
21
1 1 1 2 2 2 3 3 3 1 2 3 3 1 2 2 1 3 3 2 1
【输出样例】
6 1
2
一、 题目分析
核心诉求:
给定一个长度为N的数字序列(珠子),我们需要选择一个切分长度k。每次将序列按长度k切分成若干个连续的子串(最后不足k的部分直接丢弃)。
特殊规则在于:子串正着读和反着读被视为同一种子串 (即 (1,2,3) 和 (3,2,1) 是等价的)。
我们的目标是求出:
-
能够切出的最多不同的子串数量。
-
有多少个k能够达到这个最大数量。
-
按升序输出这些k的具体值。
数据范围 :。这意味着
的暴力算法会被无情拒绝,我们需要
或
级别的算法。
二、 思考过程与解题思路
看到子串判重,第一反应必定是字符串哈希。
难点 1:如何处理"正反同构"?
常规的字符串匹配只看正向。既然题目允许反转相等,我们可以预处理出两套哈希表:一套正向前缀哈希,一套逆向前缀哈希(相当于后缀哈希的变体)。
对于任意截取出来的子串,我们可以O(1)分别算出它的正向哈希值和逆向哈希值
。
为了统一标准,我们将**min(h1, h2)作为该子串绝对唯一的"身份证"**。这样一来,无论它在原串中是正着出现还是反着出现,最终提取出的哈希身份证都是一模一样的。
难点 2:如何枚举?会不会超时?
如果我们外层循环枚举长度k(从1到N),内层循环去按步长k遍历截取子串。内层循环究竟会执行多少次?
对于长度k,内层会切出个子串。
把所有的切分次数加起来:。
这是一个极其经典的调和级数 ,其数学极限约等于。
当N=200,000时,总的切分提取次数只有区区约240万次。这个极其微小的常数给了我们暴力枚举k的底气。
三、 算法设计
基于以上思考,核心算法流程分为三步:
-
预处理双向哈希:
-
以一个大于字符值域最大值(200000)的质数作为Base(例如200003)。
-
正向遍历计算前缀哈希
ans1。 -
逆向遍历计算后缀哈希
ans2。
-
-
枚举并提取:
-
外层循环k从1遍历到N。
-
内层循环以k为步长提取子串。取
min(正向哈希,逆向哈希)存入容器中。
-
-
容器去重统计:
-
方案 A:直接丢进
unordered_map里利用哈希表去重统计。 -
方案 B(优化):存入
vector,利用sort排序后,比较相邻元素来统计不重复元素的个数。 -
更新最大值并记录答案。
-
四、 时空复杂度分析
-
空间复杂度:需要维护正逆哈希数组以及Base的次幂数组,空间开销为O(N),对于20万的数据量完全没有压力。
-
时间复杂度:
-
unordered_map版本:总提取次数为,由于哈希表的单次操作存在较大常数,理论期望时间
,信息学奥赛一本通能过,但更严格的测试点可能因为常数问题卡在超时的边缘。
-
vector+sort版本:切分提取后对长为的数组排序,总体时间为
。虽然理论上多了一个
,但由于数组操作和
sort的底层连续内存访问有着极其优秀的极小常数,实际运行速度碾压unordered_map几条街。
-
五、 易错点总结
-
逆向哈希的提取公式:
正向哈希公式大家都很熟:
ans1[r]-ans1[l-1]*p[r-l+1]。逆向哈希数组是从n到1建立的,所以它的提取公式方向是反过来的,应当是:
ans2[l]-ans2[r+1]*p[r-l+1]。不能照抄正向的下标。 -
Base的选取:
由于珠子的颜色(即字符的值)最大可以达到
。如果你的Base取了常见的 131或13331,会导致高位的数值叠加后与低位产生极其严重的哈希碰撞(这题的构造数据很容易卡小 Base)。因此Base必须大于字符集最大值,选取质数200003是最稳妥的做法。
-
内层统计的作用域:
在使用
vector排序方案时,切忌在内层切分子串的循环内部调用sort。必须等当前长度 k所有的子串全部收集进vector后,再跳出循环进行唯一的一次排序和计数。
六、 完整代码
下面提供两个版本的代码,供对比学习。在严苛的竞赛环境下,强烈建议采用版本二 (Vector排序版) 以获取绝对安全的运行速度。
版本一:思路直观的unordered_map版
注意:由于 STL 动态内存分配和哈希冲突,此版本常数极大,虽然信息学奥赛一本题可以通过,但在部分OJ上可能会遇到测试点超时。
代码
cpp
//这个版本已经完全可以过了,但我们可以用vector来替换unordered_map
//从而让我们的速度更快
#include <iostream>
#include <unordered_map>
using namespace std;
typedef unsigned long long ull;
int n;
const int num=200003;//作为基数(基数必须大于每一位的最大值且为质数)
int a[200010];//原始每种珠子的颜色
ull p[200010];//记录num的i次方
//子串(1,2,3)和(3,2,1)是一样的 所以要正反都判断一下
ull ans1[200010];//记录正向前缀哈希
ull ans2[200010];//记录逆向后缀哈希
long long ma;//记录最多可以获得的子串数
long long b[200010];//b[i]代表每块有i个珠子 可以得到多少不同的子串
//返回闭区间[l,r]的哈希值 正向
ull gethash1(int l,int r){
if(l>r) return 0;
return ans1[r]-ans1[l-1]*p[r-l+1];
}
//返回闭区间[l,r]的哈希值 逆向
ull gethash2(int l,int r){
return ans2[l]-ans2[r+1]*p[r-l+1];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
//预处理 存储131的i次方
p[0]=1;
for(int i=1;i<=n;i++) p[i]=num*p[i-1];
//存储原始珠子颜色
for(int i=1;i<=n;i++) cin>>a[i];
//预处理存储珠子的正向哈希前缀
for(int i=1;i<=n;i++) ans1[i]=num*ans1[i-1]+a[i];
//预处理存储珠子的逆向哈希后缀
for(int i=n;i>=1;i--) ans2[i]=num*ans2[i+1]+a[i];
//枚举每一个可能的珠子长度
for(int i=1;i<=n;i++){
long long cnt=0;//当前珠子长度最多可以得到多少不同的子串
int n1=n/i*i;//最后一块小于i的就不要了
unordered_map<ull,int> m;
for(int j=1;j<=n1;j=j+i){
//最后一块小于i的就不要了
//if(j+i-1>n) break;
//如果该长度从未出现过 正反哈希都检查一遍
if(m[gethash1(j,j+i-1)]==0&&m[gethash2(j,j+i-1)]==0){
//不同长度数加一
cnt++;
//记录当前长度出现
m[gethash1(j,j+i-1)]=1;
m[gethash2(j,j+i-1)]=1;
}
}
b[i]=cnt;
ma=max(ma,cnt);
}
//先输出最大不同子串个数
cout<<ma<<" ";
//再输出能获得最大不同子串的珠子长度数
int cnt2=0;//记录能获得最大不同子串的珠子长度数
for(int i=1;i<=n;i++){
if(b[i]==ma) cnt2++;
}
cout<<cnt2<<endl;
//最后再输出所有能达到最大子串数的珠子长度
for(int i=1;i<=n;i++){
if(b[i]==ma) cout<<i<<" ";
}
return 0;
}
版本二:压常数的Vector+Sort版
推荐 :利用
min(h1, h2)生成统一身份标识,将哈希表查询转化为数组排序,彻底消除 STLmap带来的常数问题。
代码
cpp
//在前一版本的基础上算出每一个子串的正向哈希和逆向哈希取最小值作为自身的唯一身份证
//然后用vetcor排序 最后计算有多少个不同的即可
#include <iostream>
#include <vector>
#include <algorithm>//对应sort函数
using namespace std;
typedef unsigned long long ull;
int n;
const int num=200003;//作为基数(基数必须大于每一位的最大值且为质数)
int a[200010];//原始每种珠子的颜色
ull p[200010];//记录num的i次方
//子串(1,2,3)和(3,2,1)是一样的 所以要正反都判断一下
ull ans1[200010];//记录正向前缀哈希
ull ans2[200010];//记录逆向后缀哈希
long long ma;//记录最多可以获得的子串数
long long b[200010];//b[i]代表每块有i个珠子 可以得到多少不同的子串
//返回闭区间[l,r]的哈希值 正向
ull gethash1(int l,int r){
if(l>r) return 0;
return ans1[r]-ans1[l-1]*p[r-l+1];
}
//返回闭区间[l,r]的哈希值 逆向
ull gethash2(int l,int r){
return ans2[l]-ans2[r+1]*p[r-l+1];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
//预处理 存储131的i次方
p[0]=1;
for(int i=1;i<=n;i++) p[i]=num*p[i-1];
//存储原始珠子颜色
for(int i=1;i<=n;i++) cin>>a[i];
//预处理存储珠子的正向哈希前缀
for(int i=1;i<=n;i++) ans1[i]=num*ans1[i-1]+a[i];
//预处理存储珠子的逆向哈希后缀
for(int i=n;i>=1;i--) ans2[i]=num*ans2[i+1]+a[i];
//枚举每一个可能的珠子长度
for(int i=1;i<=n;i++){
long long cnt=1;//当前珠子长度最多可以得到多少不同的子串 至少一个子串
int n1=n/i*i;//最后一块小于i的就不要了
vector<ull> vec;//用来存每个子串的身份证
//当珠子长度为i时 所有的子串(不包括可能的最后一块小于k的)
for(int j=1;j<=n1;j=j+i){
//最后一块小于i的就不要了
//if(j+i-1>n) break;
//算出每一个子串的正向哈希和逆向哈希取最小值作为自身的唯一身份证
//然后存入vec
vec.push_back(min(gethash1(j,j+i-1),gethash2(j,j+i-1)));
}
//vec从小到大排序
sort(vec.begin(),vec.end());
//判断有多少个不同的子串
for(int k=1;k<vec.size();k++){
if(vec[k]!=vec[k-1]) cnt++;
}
b[i]=cnt;
ma=max(ma,cnt);
}
//先输出最大不同子串个数
cout<<ma<<" ";
//再输出能获得最大不同子串的珠子长度数
int cnt2=0;//记录能获得最大不同子串的珠子长度数
for(int i=1;i<=n;i++){
if(b[i]==ma) cnt2++;
}
cout<<cnt2<<endl;
//最后再输出所有能达到最大子串数的珠子长度
for(int i=1;i<=n;i++){
if(b[i]==ma) cout<<i<<" ";
}
return 0;
}