前言
本文主要说KMP算法、字符串哈希(BKDR)、Manacher算法、Trie树、AC自动机、扩展KMP(Z函数)、后缀数组(SA)和后缀自动机(SAM)的一些细节。
关于回文自动机(PAM),等到我再学习一下。
KMP算法
#include<iostream>
using namespace std;
int main() {
string s1,s2;
cin>>s1>>s2;
int nxt[s2.size()];
for(auto&i:nxt) i=0;
for(int i=1,j=0;i<s2.size();i++) {
while(j&&s2[i]^s2[j])
j=nxt[j-1];
if(s2[i]==s2[j]) j++;
nxt[i]=j;
}
for(int i=0,j=0;i<s1.size();i++) {
while(j&&s1[i]^s2[j])
j=nxt[j-1];
if(s1[i]==s2[j]) j++;
if(j==s2.size()) {
cout<<i-j+2<<endl;
j=nxt[j-1];
}
}
for(auto&i:nxt)
cout<<i<<' ';
return 0;
}
- 所有跳转都是j=nxt[j-1] (如果从0开始)
- nxt数组初值为0
字符串哈希
#include<iostream>
using namespace std;
int n;
bool t[100000000];
long long p[1505];
long long m=99999989;
int cnt;
int main() {
cin>>n;
p[0]=1;
for(int i=1;i<=1500;i++)
p[i]=(p[i-1]*13331)%m;
while(n--) {
string s;
cin>>s;
long long h=0;
for(int i=0;i<s.size();i++)
h=(h+s[i]*p[s.size()-i-1])%m;
if(!t[h]) cnt++,t[h]=true;
}
cout<<cnt;
return 0;
}
这里选择的质数是13331,模数是99999989。
然后是关于哈希冲突的概率分析:
绝大多数情况下,不要选择一个19级别的数,因为这样随机数据都会有Hash冲突,根据生日悖论,随便找上109个串就有大概率出现至少一对Hash 值相等的串(参见BZOJ 3098 Hash Killer II)。
最稳妥的办法是两个19级别的质数,只有模这两个数都相等才判断相等,但常数略大,代码相对难写,目前暂时没有办法卡掉这种写法(除了卡时间让它超时)(参见BZOJ 3099 Hash Killer III)。
如果能背过或在考场上找出一个1018级别的质数(Miller-Rabin),也相对靠谱,主要用于前一种担心会超时,后一种担心被卡。
偷懒的写法就是直接使用unsigned long long,不手动进行取模,它溢出时会自动进行取模,如果出题人比较良心,这种做法也不会被卡,但这个是完全可以卡的,卡的方法参见BZOJ 3097 Hash Killer I。
所以说这里的模数是很小的,很险。
Manacher算法
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
char s[11000002];
char a[11000002<<1];
int h[11000002<<1];
int n;
int main() {
cin>>s+1;
n=strlen(s+1);
a[0]='$';
a[1]='#';
for(int i=1,t=2;i<=n;i++)
a[t]=s[i],a[t+1]='#',t+=2;
n=(n<<1)+2;
a[n]='*';
int l=0,r=0;
for(int i=1;i<=n;i++) {
if(i<=r) h[i]=min(h[l+r-i],r-i+1);
while(a[i+h[i]]==a[i-h[i]]) h[i]++;
if(i+h[i]>r) r=i+h[i]-1,l=i-h[i]+1;
}
cout<<*max_element(h+1,h+1+n)-1;
return 0;
}
- 其实a[n]的地方并不需要设为’*‘,因为默认就是’\0’
- 注意更新右端点的条件。
Trie树
Trie树还可以解决最长异或路径的问题。
AC自动机
#include<iostream>
#include<queue>
using namespace std;
int t[1000005][26],top,h[1000005],nxt[1000005];
int n;
char f(char x) {
return x-'a';
}
void push(string&s) {
int x=0;
for(int i:s) {
i=f(i);
if(t[x][i]) x=t[x][i];
else {
t[x][i]=++top;
x=t[x][i];
}
}
h[x]++;
}
void bfs() {
queue<int> q;
for(int i=0;i<26;i++)
if(t[0][i])
q.push(t[0][i]);
while(!q.empty()) {
int u=q.front();
q.pop();
for(int i=0;i<26;i++) {
int v=t[u][i];
if(v) nxt[v]=t[nxt[u]][i],q.push(v);
else t[u][i]=t[nxt[u]][i];
}
}
}
int solve(string&s) {
int ans=0;
int i=0;
for(auto&x:s) {
i=t[i][f(x)];
for(int j=i;j&&~h[j];j=nxt[j])//h[j]被访问过了,那么h[j]接下来的节点肯定也被访问过了
ans+=h[j],h[j]=-1;
}
return ans;
}
int main() {
cin>>n;
for(int i=1;i<=n;i++) {
string s;
cin>>s;
push(s);
}
bfs();
string s;
cin>>s;
cout<<solve(s);
return 0;
}
代码很简单,没什么细节。
后缀数组(SA)
后缀数组主要讨论后缀数组(sa)、名次数组(rk)和高度数组(h)。获得sa数组后,通过如下性质能够很容易地O(n)求出高度数组:
h[rk[i]]>=h[rk[i-1]]-1
性质很显然。
sa数组中相邻的两个元素最为相似。
通过快速排序可以O(nlog2n)求出sa数组。通过桶排序和倍增法可以O(nlogn)求出。当然还有一些复杂方法可以O(n)求出。
后缀排序代码如下:
#include<iostream>
#include<cstring>
using namespace std;
char s[2000005];
typedef int intx[2000005];
intx f,sa,x;
int t[2000005];
//f数组
int n,m=127;
void SA() {
n=strlen(s+1);
for(int i=1; i<=n; i++) t[f[i]=s[i]]++;
for(int i=1; i<=m; i++) t[i]+=t[i-1];
for(int i=n; i; i--) sa[t[f[i]]--]=i;
for(int k=1; k<=n; k<<=1) {
memset(t,0,sizeof t);
for(int i=1; i<=n; i++) x[i]=sa[i];
for(int i=1; i<=n; i++) t[f[i+k]]++;//
for(int i=1; i<=m; i++) t[i]+=t[i-1];
for(int i=n; i; i--) sa[t[f[x[i]+k]]--]=x[i];
memset(t,0,sizeof t);
for(int i=1; i<=n; i++) x[i]=sa[i];
for(int i=1; i<=n; i++) t[f[i]]++;//
for(int i=1; i<=m; i++) t[i]+=t[i-1];
for(int i=n; i; i--) sa[t[f[x[i]]]--]=x[i];
m=0;
for(int i=1;i<=n;i++) x[i]=f[i];
for(int i=1;i<=n;i++)
if(x[sa[i]]==x[sa[i-1]]&&x[sa[i]+k]==x[sa[i-1]+k])
f[sa[i]]=m;
else
f[sa[i]]=++m;
if(m==n) return ;
}
}
int main() {
cin>>s+1;
SA();
for(int i=1;i<=n;i++)
cout<<sa[i]<<' ';
}
一些细节:
- 注意很多地方都是sa[i],不是i
- 注意桶数组t也要开到n的范围,因为f的上限是n。
高度数组有很多作用。
后缀自动机(SAM)
后缀自动机计算出后缀链接树。后缀链接树具有很多性质。
#include<iostream>
#include<vector>
using namespace std;
int T[26][2000005],fa[2000005];
long long len[2000005],cnt[2000005];
int top=1,ux=1;
void push(int x) {
int*t=T[x];
int u=ux;ux=++top;//注意ux没有int!
len[ux]=len[u]+1,cnt[ux]=1;
while(u&&!t[u]) t[u]=ux,u=fa[u];
if(!u) fa[ux]=1;
else {
int v=t[u];
if(len[v]==len[u]+1) fa[ux]=v;
else {
int vx=++top;
len[vx]=len[u]+1;//注意这一句话,别忘了
fa[ux]=vx,fa[vx]=fa[v],fa[v]=vx;
while(u&&t[u]==v) t[u]=vx,u=fa[u];
for(int i=0;i<26;i++) T[i][vx]=T[i][v];
}
}
}
vector<vector<int>> a;
long long ans;
void dfs(int u) {
for(auto&v:a[u])
dfs(v),cnt[u]+=cnt[v];
if(cnt[u]>1) ans=max(ans,(long long)len[u]*cnt[u]);
}
int main() {
string s;
cin>>s;
for(int i:s) push(i-'a');
for(int i=0;i<=top;i++) a.push_back({});
for(int i=2;i<=top;i++) a[fa[i]].push_back(i);
dfs(1);
cout<<ans;
return 0;
}
代码很简单。
后缀数组和后缀自动机有很多作用是重合的,但也有一些是不重合的。
扩展KMP(Z函数)
KMP算法对两个字符串做精确匹配,exKMP算法则做近似匹配。
#include<iostream>
#include<cstring>
using namespace std;
int z[20000005];
int p[20000005];
char a[20000005];
char b[20000005];
int main() {
cin>>b>>a;
int n=strlen(a),m=strlen(b);
z[0]=n;
a[++n]='#';
b[++m]='$';
for(int i=1,l=0,r=-1;i<n-1;i++) {
if(i<=r) z[i]=min(z[i-l],r-i+1);
while(a[z[i]]==a[i+z[i]]) z[i]++;
if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}
for(int i=0,l=0,r=-1;i<m-1;i++) {
if(i<=r) p[i]=min(z[i-l],r-i+1);
while(p[i]<n-1&&i+p[i]<m-1&&a[p[i]]==b[i+p[i]]) p[i]++;
if(i+p[i]-1>r) l=i,r=i+p[i]-1;
}
long long sum=0;
for(long long i=0;i<n-1;i++)
sum^=(i+1)*((long long)z[i]+1);
cout<<sum<<endl;
sum=0;
for(long long i=0;i<m-1;i++)
sum^=(i+1)*((long long)p[i]+1);
cout<<sum<<endl;
// for(int i=0;i<n-1;i++)
// cout<<z[i]<<' ';
// cout<<endl;
// for(int i=0;i<m-1;i++)
// cout<<p[i]<<' ';
}
- 第二次做匹配时while要限制边界条件,防止数组越界。
- 注意更新右端点的条件
后记
于是皆大欢喜。