题记
蒟蒻发现自己的字符串算法基本上要丢完了……还是要捡一捡……
Round1 R o u n d 1 - KMP K M P
所以说作为一个字符串匹配算法,
KMP
K
M
P
还是很重要的。尤其是
KMP
K
M
P
的神奇
next
n
e
x
t
数组,可以解决循环串问题和一般的字符串匹配问题……
具体步骤:
1. 求出模式串的
next
n
e
x
t
数组
2. 匹配
所以敲一个代码试试吧……
求
next
n
e
x
t
数组的代码
void getnext(char *A,int *nex,int n)
{
memset(nex,0,sizeof(nex));
nex[0]=-1;
int i=0,j=-1;
while(i<n)
{
if(j==-1||A[i]==A[j])
nex[++i]=++j;
else
j=nex[j];
}
}
匹配代码最简单了~
int KMP(char *A,char *B,int *nex,int n,int m)
{
int i=0,j=-1;
while(i<m&&j<n)
{
if(j==-1||B[i]==A[j])
i++,j++;
else
j=nex[j];
}
if(j==n)
return 1;
return 0;
}
可以发现求 next n e x t 数组的过程其实与匹配的过程十分相像,只不过变成了模式串自我匹配而已。
Round2 R o u n d 2 - exKMP e x K M P
曾经 too naive t o o n a i v e 的我(虽然现在也很 naive n a i v e )一度写下过这样的话
貌似KMP可以解决的问题exKMP都能解决,而exKMP能解决的问题KMP不一定能解决
然后在一道循环串题目中被无情打脸,因为它要求的是每一个前缀是否是循环串,然后要输出最大循环节长度,用 exKMP e x K M P 做很困难……(如果错了,望轻喷)
不过
exKMP
e
x
K
M
P
确实是个不错的算法,快要忘完的我再次回忆一下写法
求出
next
n
e
x
t
void getnext(char *A,int *nex,int n)
{
memset(nex,0,sizeof(nex));
nex[0]=n;
for(int i=1;i<n;i++)
if(A[i]!=A[i-1])
break;
else
nex[1]++;
int po=1,maxp=nex[1]+1;
for(int i=2;i<n;i++)
{
if(i+nex[i-po]<maxp)nex[i]=nex[i-po];
else nex[i]=max(maxp-i,0)
while(i+nex[i]<n&&A[nex[i]]==A[i+nex[i]])nex[i]++;
if(i+nex[i]>maxp)maxp=i+nex[i],po=i;
}
}
然后是匹配 B B 串的时候,很类似的啦……
void exKMP(char *A,char *B,int *nex,int *ex,int n,int m)
{
for(int 0=1;i<n&&i<m;i++)
if(A[i]!=B[i])
break;
else
ex[0]++;
int po=0,maxp=ex[0];
for(int i=1;i<m;i++)
{
if(i+nex[i-po]<maxp)ex[i]=nex[i-po];
else ex[i]=max(maxp-i,0)
while(ex[i]<n&&i+ex[i]<m&&A[ex[i]]==B[i+ex[i]])ex[i]++;
if(i+ex[i]>maxp)maxp=i+ex[i],po=i;
}
}
- manacher m a n a c h e r
记得最清楚的一个算法……由于奇串和偶串的特判让人心烦,所以我们添加奇怪的字符(不出现在原串中,一般是# @ &等)在两个相邻字符中,然后在头尾再增加另外的字符避免查找时溢出的特判
然后,我们记录一下
po
p
o
和
maxp(po+len[po])
m
a
x
p
(
p
o
+
l
e
n
[
p
o
]
)
(是不是似曾相识?),表示某一个以
po
p
o
为中心的回文串右端点为
maxp
m
a
x
p
(必须是最靠右的),找到
i
i
关于 的对称点
2∗po−i
2
∗
p
o
−
i
,如果
len[2∗po−i]<maxp−i
l
e
n
[
2
∗
p
o
−
i
]
<
m
a
x
p
−
i
,根据回文的对称可得
len[i]=len[2∗po−i]
l
e
n
[
i
]
=
l
e
n
[
2
∗
p
o
−
i
]
,否则就不确定了,但有一点可以确定,
len[i]>=maxp−i
l
e
n
[
i
]
>=
m
a
x
p
−
i
,剩下的部分枚举就好了,由于一旦被枚举过,就会被
maxp
m
a
x
p
覆盖,因此复杂度还是
O(n)
O
(
n
)
的
int manachar(char *A,int *len)//注意这里的A已经预处理过了
{
for(int i=1;i<=2*n+1;i++)
{
if(i<mx)len[i]=min(mx-i,len[2*po-i]);
else len[i]=1;
while(A[i+len[i]]==A[i-len[i]])len[i]++;
if(len[i]+i>mx)
mx=len[i]+i,po=i;
ans=max(ans,len[i]);
}
}
Round4 R o u n d 4 - AC A C 自动机
KMP
K
M
P
的升级版,但仍然很快,把所有的模式串建成一颗
Trie
T
r
i
e
树,再加上
fail
f
a
i
l
指针(与
next
n
e
x
t
数组类似),就可以实现多模式串匹配,但是要注意的是,需要在匹配成功后将
fail
f
a
i
l
链上所有的点都打上标记(不然不就和只在一个串上匹配一样了吗……),然后有一个很神的优化,把空指针连到
fail
f
a
i
l
的对应位置(
fail
f
a
i
l
)
话不多说上代码(以小写字母为例)
struct Trie
{
int cnt,nex[30],fail;
}A[N];
void insert(char *S,int w)
{
int len=strlen(S),val,r=root;
for(int i=0;i<len;i++)
{
val=S[i]-'a';
if(!A[r].nex[val])
A[r].nex[val]=++tot;
r=A[r].nex[val];
}
A[r].cnt=w;
}
void build()
{
int r=root;
A[r].fail=r;
q[tail++]=r;
while(head<tail)
{
r=q[head++];
for(int i=0;i<26;i++)
{
int ch,p;
if((ch=A[r].nex[i]))
{
q[tail++]=ch;
for(p=A[r].fail;p!=root&&!A[p].nex[i];p=A[p].fail);
int tmp=A[p].nex[i];
if(tmp&&tmp!=ch)A[ch].fail=tmp;
else A[ch].fail=root;
}
else
{
for(p=A[r].fail;p!=root&&!A[p].nex[i];p=A[p].fail);
int tmp=A[p].nex[i];
if(tmp)A[r].nex[i]=tmp;
else A[r].nex[i]=root;
}
}
}
}
void query(char *S,int w)
{
memset(vis,0,sizeof(vis));
int sum=0,len=strlen(S),val,r=root;
for(int i=0;i<len;i++)
{
val=S[i]-'a';
r=A[r].nex[val];
for(int p=r;p!=root&&vis[p]!=-1;p=A[p].fail)
{
vis[p]=-1;
if(A[p].cnt)
vir[++sum]=A[p].cnt;
}
}
printf("%d\n",sum);
}
Round5 R o u n d 5 -后缀数组
啊啊啊啊啊——最不熟的就是这玩意了……
后缀数组的代码只会写
O(nlogn)
O
(
n
log
n
)
的倍增,具体步骤呢就是基数排序啦……
然后还有一个
height
h
e
i
g
h
t
数组,非常常用,所以这个模板还是要背下来才好啊……
详情请见代码
int sa[N],rk[N],ht[N],wa[N],wb[N],ws[N],wv[N],n,m;
//m是长度n与字符集大小的最大值
char S[N];
//下标从1开始
void Da()
{
int *x=wa,*y=wb,*t;
for(int i=0;i<=m;i++)ws[i]=0;
for(int i=1;i<=n;i++)ws[x[i]=S[i]]++;
for(int i=1;i<=m;i++)ws[i]+=w[i-1];
for(int i=1;i<=n;i++)sa[ws[x[i]]--]=i;
for(int j=1,p=1;p<n;j<<=1)
{
p=0;
for(int i=n-j+1;i<=n;i++)y[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>j)y[++p]=sa[i]-j;
for(int i=1;i<=n;i++)wv[i]=x[y[i]];
for(int i=0;i<=m;i++)ws[i]=0;
for(int i=1;i<=n;i++)ws[wv[i]]++;
for(int i=1;i<=m;i++)ws[i]+=ws[i-1];
for(int i=1;i<=n;i++)sa[ws[wv[i]]--]=y[i];
t=x;x=y;y=t;
x[sa[1]]=p=1;
for(int i=2;i<=n;i++)
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+j]==y[sa[i-1]+j])x[sa[i]]=p;
else x[sa[i]]=++p;
}
}
void Ca()
{
for(int i=1;i<=n;i++)rk[sa[i]]=i;
int k=0;
for(int i=2;i<=n;i++)
{
if(k)k--;
for(int j=sa[rk[i]-1];S[i+k]==S[j+k];k++)
ht[rk[i]]=k;
}
}
Round6 R o u n d 6 -后缀自动机
同样非常的迷……看了很多博客才有一点点懂了……
首先,将后缀自动机构造出来,其中有一个指针
link
l
i
n
k
,可以构成一棵树,这棵树的每一个叶子都恰好对应了一个串的后缀,有很多的恶心性质,可以解决很多
magical
m
a
g
i
c
a
l
的问题,但是并不会证明这些性质。
btw
b
t
w
,貌似只有我认为后缀自动机比后缀数组好写?(也可能是我太弱了,然而还是基本靠背才会写)
void insert(int c)
{
int p,cur=++cnt;
A[cur].len=A[last].len+1;
for(p=last;p!=-1&&!A[p].nex[c];p=A[p].link)
A[p].nex[c]=cur;
if(p<0)
A[cur].link=0;
else
{
int q=A[p].nex[c];
if(A[q].len==A[p].len+1)
A[cur].link=q;
else
{
int clone=++cnt;
A[clone]=A[q];
A[clone].len=A[p].len+1;
for(;p!=-1&&A[p].nex[c]==q;p=A[p].link)
A[p].nex[c]=clone;
A[q].link=A[cur].link=clone;
}
}
last=cur;
}
The end T h e e n d
好了就到这里了,不够完整的话以后再填坑(加入有生之年系列)