提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 一.最小表示法
- 二.字符串哈希r
- 三.AC自动机
- 四.Manacher
前言
本篇文章对字符串的一些算法做了总结
一、最小表示法
https://www.luogu.com.cn/problem/P1368
给定一个字符串a,可以把字符串的第一个字符移动到最后,可以操作任意多次,经过n次的移动后,形成字典序最小的字符串是什么,可以明显地看出经过n次移动后所形成的字符串,就是在原有字符串的基础上选择一个位置,从该位置上输出到最后,再从头输出到该位置就是改变后的字符串,我们要找的最小表示就是去找这个位置。
如何去找?
可以设置两个指针l和r,让l的初值为0,也就是指向第一个位置,让r的初值为1,指向第二个位置同时设置一个k初值为0,依次比较a[(l+k)%n]和a[(r+k)%n],这里要取余,为了回到左边的位置否则会超过字符串长度,如果一样就让k++,若前面那个大就让l指向l+k+1的位置,否则就让让r指向r+k+1的位置,最后返回min(l,r),注意每次改变l和r的位置后都要让k=0。
总得来说就是不断更新l和r的位置,最后找到一个最小表示的开始地方,复杂度为O(n).
代码如下;
int zx(string a)
{
int l=0,r=1,k=0;
int n=a.length();
while(l<n&&r<n&&k<n)
{
if(a[(l+k)%n]==a[(r+k)%n])k++;
else
{
if(a[(l+k)%n]>a[(r+k)%n])l+=k+1;
else r+=k+1;
if(r==l)r++;
k=0;
}
}
return min(l,r);//返回最小最小表示的开始位置
}
二、字符串哈希
所谓的字符串哈希,就是让每个不同的字符串对应一个固定的数字,至于如何实现,可以设置一个base和取余数mod,这里的base和mod最好用质数,这样可以让冲突最小,h数组表示一个字符串的前缀哈希值,用p数组表示base的幂次值,h数组可以递推来求,h[i]=h[i-1]*base+str[i]-'a'+1,在求每一端的哈希值时要注意,根据前缀和的求法,本来直接让h[r]-h[l-1]即可,但是h[r]和h[l-1]在前面的数的幂次是不一样的所以h[l-1]需要再乘上base的r-l+1次才可以,因为h[r]的最高幂次是r-1而h[l-1]的最高幂次是l-2。
这里用了unsigned long long 的自然溢出,否则会导致数值太大溢出
代码如下
typedef unsigned long long ull;
const int maxn=1e6+7;
const int seed=131;
char str[maxn];
ull h[maxn],p[maxn];//h数组是求前缀哈希值,p数组是求进制的i次幂
//求一段区间的哈希值
ull get(int l,int r){ return h[r]-h[l-1]*p[r-l+1]; }
int main(){
scanf("%s",str+1);
int n=strlen(str+1);
p[0]=1;
for(int i=1;i<=n;i++){
h[i]=h[i-1]*seed+str[i]-'a'+1;
//注意:“+1”不能省,否则“agh”与“gh”表示用一个数,也可以写h[i]=h[i-1]*seed+str[i];
p[i]=p[i-1]*seed;
}
}
三.AC自动机
https://www.luogu.com.cn/problem/P3808
上面是AC自动机的模板题
AC自动机感觉就是kmp和trie数的结合体,在字典树上的每一个节点都有一个fail值,我们称它为失配指针,具体的某个结点的fail值指的以这个结点为结尾的后缀和以fail值为结尾的字串(包括fail这个点)有最大公共部分,至于如何实现fail指针的创建,我们可以用BFS去实现,因为每个结点的fail总是指向比它深度更浅的结点,那到底怎么指呢,其实就是指向它父亲结点的fail的相同儿子处,感觉其实是一个递归的过程。
在这道题中,我们创建好trie树,并设置好fail指针,对模式串进行匹配时,要对每一个模式串的字符跳fail值,直到没有fail可跳后再去到下一个字符,就这样依次遍历每一个字符,对于每一个字符都去统计一下看出现了几个字串,记住每次统计好一个串后要标记一下,防止该字串在模式串中多次出现。当然有时候这样跳也会TLE,所以有时候我们记一下初始的位置,最后再统一走fail就好了。
代码如下:
int id,n;
struct tree{
int vis[26];
int fail;
int cnt;
}trie[maxn];
void build(string a)
{
int k=0,len=a.size();
for(int i=0;i<len;i++)
{
int x=a[i]-'a';
if(trie[k].vis[x]==0)trie[k].vis[x]=++id;
k=trie[k].vis[x];
}
trie[k].cnt++;
}
void get_fail()
{
queue<int>q;
for(int i=0;i<26;i++)
{
if(trie[0].vis[i]!=0)
{
trie[trie[0].vis[i]].fail=0;
q.push(trie[0].vis[i]);
}
}
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=0;i<26;i++)
{
if(trie[u].vis[i]!=0)
{
trie[trie[u].vis[i]].fail=trie[trie[u].fail].vis[i];//定义fail的值
q.push(trie[u].vis[i]);
}
else trie[u].vis[i]=trie[trie[u].fail].vis[i];
}
}
}
int query(string a)
{
int ans=0,len=a.length();
int now=0;
for(int i=0;i<len;i++)
{
now=trie[now].vis[a[i]-'a'];
for(int t=now;t&&trie[t].cnt!=-1;t=trie[t].fail)
{
ans+=trie[t].cnt;
trie[t].cnt=-1;
}
}
return ans;
}
四.Manacher
马拉车算法的最大优势就是可以在O(n)的复杂度内求出最大的回文串长度
我们要先对字符串进行一个预处理,在每个字符的两边加上一个相同的符号,同时在开头和结尾加上一个都没有出现过的字符作为结束字符,这样可以让不管是奇数还是偶数的回文串都被处理出来。
我们先设置一个id用来表示当前能够达到的最右边回文串的中心,mx表示以id为中心的回文串最右边的位置,len[i]数组表示i位置的回文串半径,可以发现len[i]-1就是原来字符串的回文串长度,那我们只要求出所有的len数组即可
依次去遍历每一个字符去求len,当前位置为i若i<mx说明在id的左边有另一个和str[i]匹配的字符此时len[i]的长度至少是min(mx-i,len[2*id-i]),2*id-i就是i关于id的对应位置,但是大于mx位置的字符我们还不知道,所以这里要暴力去匹配,若i>mx,则len[i]就只能先是1,再去暴力匹配,更新len[i]的值,最后不要忘记更新id和mx
代码如下:
char s[11000007];
char str[25000007];
int len[25000005];
int initstr(char *s)
{
int k=0,len=strlen(s);
str[k++]='@';
for(int i=0;i<len;i++){
str[k++]='#';
str[k++]=s[i];
}
str[k++]='#';
str[k]='\0';
return k;
}
int manacher(int n)
{
int mx=0,id=0,maxx=0;
for(int i=1;i<n;i++)
{
if(mx>i)
len[i]=min(mx-i,len[2*id-i]);
else
len[i]=1;
while(str[i+len[i]]==str[i-len[i]])
len[i]++;
if(len[i]+i>mx)
{
mx=i+len[i];
id=i;
maxx=max(maxx,len[i]);
}
}
return maxx-1;
}