前言
字符串哈希,非常非常好用NB的方法,虽然有一定概率会翻车(翻车概率极低),但是这个是真的NB,你如果会这个在大多数场合可以避免掉有一些算法的学习,比如马拉车算法,
你学马拉车只能处理回文字符串,虽然马拉车是O(N) ,但在非极端测试点的情况下我们可以用字符串哈希来偷鸡,字符串哈希简单好用,比KMP简单很多。(KMP有不可替代的场合,KMP还是有必要的)字符串哈希能处理非常非常多的情况,话不多说来道经典的题:
字符串哈希
给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数l1,r1,l2,r2,请你判断[l1,r1]和[l2,r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数n和m,表示字符串长度和询问次数。
第二行包含一个长度为n的字符串,字符串中只包含大小写英文字母和数字。
接下来m行,每行包含四个整数l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从1开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出“Yes”,否则输出“No”。
每个结果占一行。
#include<iostream>
#include<algorithm>
#include<cstring>
#define ull unsigned long long
using namespace std;
const int N=100010,base=131;
char str[N];
ull p[N],h[N];
int n,m;
int get(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
int main(){
//freopen("data.in","r",stdin);
//freopen("data.out","w",stdout);
scanf("%d%d",&n,&m);
scanf("%s",str+1);
p[0]=1;
for(int i=1;i<=n;i++)
{
p[i]=p[i-1]*base;
h[i]=h[i-1]*base+str[i]-'a'+1;
}
while(m--)
{
int l1,l2,r1,r2;
cin>>l1>>r1>>l2>>r2;
if(get(l1,r1)!=get(l2,r2)) puts("No");
else puts("Yes");
}
return 0;
}
原理就是用131进制来表示一个字符串(131不容易翻车),原理非常简单,蒟蒻我笔拙主要是懒 ,如果想听更好的讲解请移步至ACwing。还有就是为了保证代码查询速度为O(1)将131n保存下来.
回文子串的最大长度
如果一个字符串正着读和倒着读是一样的,则称它是回文的。
给定一个长度为N的字符串S,求他的最长回文子串的长度是多少。
输入格式
输入将包含最多30个测试用例,每个测试用例占一行,以最多1000000个小写字符的形式给出。
输入以一个以字符串“END”(不包括引号)开头的行表示输入终止。
输出格式
对于输入中的每个测试用例,输出测试用例编号和最大回文子串的长度(参考样例格式)。
每个输出占一行。
这是经典的马拉车的题目(数据量很恐怖,但O(nlogn)不至于炸),但如果我们会字符串哈希一样可以偷鸡。
#include<iostream>
#include<algorithm>
#include<cstring>
#define ull unsigned long long
using namespace std;
const int N=2000010,base=131;
ull p[N],hl[N],hr[N];
char str[N];
ull get(ull h[],int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
int main(){
int T=0;
while(scanf("%s",str+1),strcmp(str+1,"END"))
{
int n=strlen(str+1);
for(int i=2*n;i;i-=2)
{
str[i]=str[i/2];
str[i-1]='z'+1;
}
n*=2;
p[0]=1;
for(int i=1,j=n;i<=n;i++,j--)
{
p[i]=p[i-1]*base;
hl[i]=hl[i-1]*base+str[i]-'a'+1;
hr[i]=hr[i-1]*base+str[j]-'a'+1;
}
int res=0;
for(int i=1;i<=n;i++)
{
int l=0,r=min(i-1,n-i);
while(l<r)
{
int mid=l+r+1>>1;
if(get(hl,i-mid,i-1)!=get(hr,n-(i+mid)+1,n-(i+1)+1)) r=mid-1;
else l=mid;
}
if(str[i-l]<='z')res=max(res,l+1);
else res=max(res,l);
}
printf("Case %d: %d\n",++T,res);
}
return 0;
}
可以看见代码非常的简洁,我们防止字符串出现偶数,用插孔法把他变成奇数。
然后以所有点为中点二分枚举半径。代码非常的简单,没什么好说的。
后缀数组
后缀数组 (SA) 是一种重要的数据结构,通常使用倍增或者DC3算法实现,这超出了我们的讨论范围。
在本题中,我们希望使用快排、Hash与二分实现一个简单的O(nlog2n)的后缀数组求法。
详细地说,给定一个长度为 n 的字符串S(下标 0~n-1),我们可以用整数 k(0≤k<n) 表示字符串S的后缀 S(k~n-1)。
把字符串S的所有后缀按照字典序排列,排名为 i 的后缀记为 SA[i]。
额外地,我们考虑排名为 i 的后缀与排名为 i-1 的后缀,把二者的最长公共前缀的长度记为 Height[i]。
我们的任务就是求出SA与Height这两个数组。
输入格式
输入一个字符串,其长度不超过30万。
输出格式
第一行为数组SA,相邻两个整数用1个空格隔开。
第二行为数组Height,相邻两个整数用1个空格隔开,我们规定Height[1]=0。
后缀数组,很多被这个题目的名字给吓到了,我没学过后缀数组怎么办,没关系,我也不会 我们可以用字符串哈希偷鸡,先看代码。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<limits.h>
#define ull unsigned long long
using namespace std;
const int N=300010,base=131;
char str[N];
ull h[N],p[N];
int n,sa[N];
ull get(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
int get_common_pre(int a,int b)
{
int l=0,r=min(n-a+1,n-b+1);
while(l<r)
{
int mid=l+r+1>>1;
if(get(a,a+mid-1)!=get(b,b+mid-1)) r=mid-1;
else l=mid;
}
return l;
}
bool CMP(int a,int b)
{
int len=get_common_pre(a,b);
int ra=a+len>n?INT_MIN:str[a+len];
int rb=b+len>n?INT_MIN:str[b+len];
return ra<rb;
}
int main(){
scanf("%s",str+1);
n=strlen(str+1);
p[0]=1;
for(int i=1;i<=n;i++)
{
p[i]=p[i-1]*base;
h[i]=h[i-1]*base+str[i]-'a'+1;
sa[i]=i;
}
sort(sa+1,sa+n+1,CMP);
for(int i=1;i<=n;i++) printf("%d ",sa[i]-1);
puts("");
for(int i=1;i<=n;i++)
{
if(i==1) printf("0 ");
else printf("%d ",get_common_pre(sa[i],sa[i-1]));
}
puts("");
return 0;
}
我们利用字符串哈希+快排+二分完成偷鸡。
假设字符串"ponoiiipoi" n=10
后缀数组的意思就是:
i | str |
---|---|
10 | i |
9 | oi |
8 | poi |
7 | ipoi |
6 | iipoi |
5 | iiipoi |
4 | oiiipoi |
3 | noiiipoi |
2 | onoiiipoi |
1 | ponoiiipoi |
我们利用字符串哈希的特点用二分法求得两个后缀数组的最长公共前缀,然后完成CMP函数,快排就得到了答案,代码非常的简洁,这样就避免了那些,我们看不懂的高级算法了。~ 。~