字符串匹配

前缀函数(π\pi函数)

定义

给定一个长为nn字符串SS,其前缀函数被定义为一个长度为nn的数组π\pi。其中π\pi的定义是:如果子串s[0...i]s[0...i]有一对或多对相等的真前缀与后前缀,π[i]\pi[i]等于他们相等的最长长度,没有则为0.

具体实现

遍历字符串,每次都从最大的π\pi开始比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vector<int> piFunction(string s)
{
int n = s.length();
vector<int> pi(n);
for(int i=0;i<n;i++)
{
for(int j=i;j>=0;j--)
{
if(s.substr(0,j)==s.substr(i-j+1,i))
{
pi[i]=j;
break;
}
}
}
return pi;
}

优化

s[i+1]=s[π[i]]s[i+1]=s[\pi[i]]时很容易得到,对于π[i+1]\pi[i+1]我们知道他最大等于π[i]+1\pi[i]+1,所以在第二层循环中jj可以从π[i]+1\pi[i]+1开始。

s[i+1]!=s[π[i]]s[i+1]!=s[\pi[i]]时,我们需要从j0=π[i]j^0=\pi[i]不断往前寻找j(k+1)=π[i+1]j^{(k+1)}=\pi[i+1],即s[i+1]=s[jk+1]s[i+1]=s[j^{k+1}]。通过图片我们可以知道,经过对π\pi的维护此时A,B,CA,B,C段是完全相同的,所以当最后一个字符不匹配时代表对于当前的jkj^{k}其后面π[j1]\pi[j-1]个数都不可能匹配。

这样我们可以将前面jj--(此处为k++k++)的过程替换成j=π[j1]j=\pi[j-1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vector<int> piFunction(string s)
{
int n = s.length();
vector<int> pi(n, 0);
for (int i = 1; i < n; i++)
{
int j = pi[i - 1];
while (j != 0 && s[i] != s[j])
j = pi[j - 1];
if (s[i] == s[j])
j++;
pi[i] = j;
}
return pi;
}

KMP

给出一个长为nn字符串ss和一个文本tt,求出sstt中的所有出现。

我们可以构造一个新字符串cur=s+#+tcur=s+'\#'+t其中#\#为既不出现在ss也不出现在tt中的字符,然后求出他的π\pi函数。由于#\#的存在,很容易得到π[i]\pi[i]的值不会超过nn。当π[i]=n\pi[i]=n时,代表此时s[0...n1]=cur[in+1...i]s[0...n-1]=cur[i-n+1...i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pair<int, vector<int>> kmp(string s, string t)
{
int n = s.length();
int m = t.length();
string cur = s + '#' + t;
vector<int> pi = piFunction(cur);
vector<int> show;
int num = 0;
for (int i = n; i < m + n + 1; i++)
{
if (pi[i] == n)
{
num++;
show.push_back(i + 1 - n - n);
}
}
return {num, show};
}