单模匹配:KMP与扩展KMP
一,KMP 匹配
题面:所谓字符串匹配,是这样一种问题:“字符串 P 是否为字符串 S 的子串?如果是,它出现在 S 的哪些位置?”
约定:其中 S 称为主串;P 称为模式串
朴素思考
枚举每个S中长度为
s
t
r
l
e
n
(
P
)
strlen(P)
strlen(P)的子串,直接判断字符串相等
在这个过程中,可能直到
l
e
n
(
P
)
−
1
len(P)-1
len(P)−1 位的时候,串子还是匹配的,不过在最后一位失败了,那么下一次还得重头来,好可惜!
所以使用多余的(无数个小匹配段子)信息,就是 KMP 的精髓
标准思考
要素:
n
e
x
t
[
i
]
next[i]
next[i] 数组,代表了:
在 P 中以
i
i
i 为 后缀(结尾元素) 的长度为
n
e
x
t
[
i
]
next[i]
next[i] 的串和 从头开始的长度为
n
e
x
t
[
i
]
next[i]
next[i] 的串是完全一样的
数学语言:
P
[
1
,
n
x
t
[
i
]
]
=
=
P
[
i
−
n
x
t
[
i
]
+
1
,
i
]
P[1,nxt[i]]==P[i-nxt[i]+1,i]
P[1,nxt[i]]==P[i−nxt[i]+1,i],
l
e
n
=
n
x
t
[
i
]
len=nxt[i]
len=nxt[i]
注意:下面的图片下标从0开始
在 S[0] 尝试匹配,失配于 S[3] <=> P[3] 之后,我们直接把模式串往右移了两位,让 S[3] 对准 P[1]. 接着继续匹配,失配于 S[8] <=> P[6], 接下来我们把 P 往右平移了三位,把 S[8] 对准 P[3]. 此后继续匹配直到成功。
下一步匹配时,如图中蓝色箭头所示,旧的后缀要与新的前缀一致(如果不一致,那就肯定没法匹配上了)!
回忆next数组的性质:
P
[
0
]
P[0]
P[0] 到
P
[
i
]
P[i]
P[i] 这一段子串中,前
n
e
x
t
[
i
]
next[ i ]
next[i] 个字符与后
n
e
x
t
[
i
]
next[i]
next[i] 个字符一模一样。
既然如此,如果失配在
P
[
r
]
P[r]
P[r], 那么
P
[
0
]
P
[
r
−
1
]
P[0]~P[r-1]
P[0] P[r−1] 这一段里面,前
n
e
x
t
[
r
−
1
]
next[r-1]
next[r−1] 个字符恰好和后
n
e
x
t
[
r
−
1
]
next[r-1]
next[r−1] 个字符相等——也就是说:
我们可以拿长度为
n
e
x
t
[
r
−
1
]
next[r-1]
next[r−1] 的那一段前缀,来顶替当前后缀的位置,让匹配继续下去!
代码实现
注意:下标1起步
求解
n
x
t
nxt
nxt 数组就是P和P匹配,步骤一致
前置知识:双指针
注意:P配 P 的时候,从第二位开始
int main()
{
cin>>n>>p+1>>m>>s+1;
for (int j = 0,i = 2; i <= n; i ++ )
{
while (j&&p[j+1]!=p[i])j=nxt[j];
if(p[j+1]==p[i])j++;
nxt[i]=j;
}
for (int i = 1,j=0; i <= m; i ++ )
{
while (j&&p[j+1]!=s[i])j=nxt[j];
if(p[j+1]==s[i])j++;
if(j==n) printf("%d ",i-j),j=nxt[j];
}
}
二,扩展KMP(Z 算法)
引入Z数组
- 表示以
i
开头的后缀和s
的前缀的最长公共子串(LCP) - 特别注意
z[0]=0
- 和自动机一个意思,需要一个接受和终止状态,用已知的信息缩减未知的部分的运算
流程
- 双指针
l,r
表示已知的右端点最靠右的z[i]
- 分成
i
,在r的左边和在r的右边
1,r包含i的时候:
这是绿色段是一个已知的匹配成功的段(上图 a
对应 l
,p
对于 r
,next
即为 z
数组)
易知在绿色段内,
s
[
i
]
−
s
[
p
]
s[i]-s[p]
s[i]−s[p]可以等效为
s
[
0
]
+
s
[
z
[
a
]
]
s[0]+s[z[a]]
s[0]+s[z[a]],所以他的信息可以等效移植
z [ i ] = m i n ( z [ i − l ] , r − i + 1 ) z[i]=min(z[i-l],r-i+1) z[i]=min(z[i−l],r−i+1)
- 如果等效的 z [ i − l ] z[i-l] z[i−l]没到 r r r 的那个相对位置就失配了,那 i i i 必然没到 r r r 就失配
- 超出 r − i + 1 r-i+1 r−i+1的部分暴力判断即可,记得最后更新 r r r 与 l l l
2, i超出r的时候
直接暴力判断即可,记得最后更新 r r r 与 l l l
void SOL_Z(char s[])
{
int l,r;
l=r=0;
rep(i,1,len-1)
{
if(i<=r && z[i-l]<r - i + 1) z[i]=z[i-l];
else
{
z[i]=max(0,r-i+1);
while(i+z[i]<len && s[i+z[i]]==s[z[i]])z[i]++;
}
if(z[i]+i-1>r)r = z[i]+i-1,l=i;
}
}
解题思路
常用技巧
反串,翻转,拼串,分割,分组
一,单模匹配
- 把要匹配的串前置,中间用一个分割字符,运行匹配,对于每一个在待匹配串中的位置,只要对应和模式串相同的 Z [ i ] Z[i] Z[i],由于分割字符的存在,必然表示一个匹配成功的位置
二,本质不同子串
- 定义: 一个字符串的子串中,长度不同或者长度相同但相同位置的字母不同的子串个数
- 做法:考虑增量
1,已知一个子串的本质不同子串数,那么新加入一个字符,本质不同子串会加一些
2,考虑建立新串的反串,求他的height或者z
3,存在一个 z ( m a x ) z(max) z(max) 使得以新字符 c c c为结尾,长度小于 z ( m a x ) z(max) z(max) 的子串已经出现过了,那么新的本质不同子串个数就是 ∣ t ∣ − z ( m a x ) |t|- z(max) ∣t∣−z(max) - 复杂度平方,只是用于思路
三,最小循环节
- 其整周期的长度为最小的字符串长度的因数 i i i ,满足 z [ i ] + i = = l e n z[i]+i==len z[i]+i==len
- 满足倍数方能整数的分割,满足后缀前缀拼全方能有循环