形成给定字符串所需的最小前缀和后缀的数量

什么是最小前缀和后缀?

在计算机科学中,最小前缀和后缀是指一个字符串的前缀(不包括它本身)和后缀(不包括它本身)中,最小共有的字符数量。例如,字符串"abcdab"的前缀为"a", "ab", "abc", "abcd", "abcda",后缀为"b", "ab", "dab", "cdab", "bcdab",其中最小共有的字符数量为2,即"ab"。

在字符串匹配中,最小前缀和后缀是一个非常重要的概念。通过计算给定字符串的最小前缀和后缀的数量,可以确定字符串匹配的有效边界。

朴素算法

算法思想

朴素算法也叫暴力算法,是一种简单直接的字符串匹配算法。其基本思想是,将模式串与文本串的每个子串进行比较,找到匹配的子串。

在朴素算法中,计算最小前缀和后缀的数量的方式是,对于每个子串,从头开始往后依次比较每个字符,直到找到最大的共同前缀和后缀。

int minPrefixSuffix(string s) {

int n = s.size();

for (int i = n - 1; i >= 1; i--) {

bool flag = true;

for (int j = 0; j < i; j++) {

if (s[j] != s[n - i + j]) {

flag = false;

break;

}

}

if (flag) {

return i;

}

}

return 0;

}

这个算法的时间复杂度为$O(n^3)$,其中$n$为字符串的长度,显然不够高效。

算法改进

我们可以观察到,计算最小前缀和后缀的数量的过程涉及到了很多次重复比较。例如,在计算子串"s[0:i]"和子串"s[n-i:n]"的最大共同前缀和后缀的时候,我们已经比较了子串"s[0:i-1]"和"s[n-i+1:n]"的最大共同前缀和后缀了。

因此,我们可以通过记录已经算过的最大共同前缀和后缀,来避免重复比较。我们定义数组$pi$,其中$pi[i]$表示子串$s[0:i]$的最大共同前缀和后缀的长度。

假设我们已经计算出了$pi[i-1]$,现在来计算$pi[i]$。我们发现,如果$s[pi[i-1]]$和$s[i]$相等,那么$s[0:i]$的最大共同前缀和后缀就是$s[0:pi[i-1]+1]$的最大共同前缀和后缀再加1。否则,我们需要在已知的$pi[i-1]$和$s[0:i-1]$的最大共同前缀和后缀中找到更小的一个,这样才能使$s[0:i]$和$s[n-i:n]$的最大共同前缀和后缀也更小。

vector<int> prefixFunction(string s) {

int n = s.size();

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;

}

int minPrefixSuffix(string s) {

int n = s.size();

vector<int> pi = prefixFunction(s);

return pi[n - 1];

}

这个算法的时间复杂度为$O(n)$,其中$n$为字符串的长度。我们可以发现,通过利用已知的信息,避免重复计算,算法的效率得到了极大的提高。

KMP算法

算法思想

KMP算法是由Donald Knuth、Vaughan Pratt和James Morris于1977年联合发表的,它是一个高效的字符串匹配算法,可以在$O(m+n)$的时间复杂度内找到模式串在文本串中的位置,并且可以计算出模式串的最小前缀和后缀的数量。

我们已经知道了如何计算模式串的最小前缀和后缀的数量,现在的问题是如何在匹配字符串的过程中同时计算它。

与朴素算法相同,我们需要先预处理文本串和模式串的最大共同前缀和后缀。我们定义数组$pi$,其中$pi[i]$表示模式串$pat[0:i]$的最大共同前缀和后缀的长度。

考虑在匹配字符串的过程中,如何基于已知的$pi$来计算匹配上的最小前缀和后缀的数量。

假设文本串匹配到了位置$i$,模式串匹配到了位置$j$,那么最小前缀和后缀的数量为$j - pi[j-1]$。

这个结论的依据是,由于文本串和模式串是从左往右进行匹配,匹配上的长度肯定不会超过$j$,而由于$pi[j-1]$表示模式串$pat[0:j-1]$的最大共同前缀和后缀的长度,所以匹配上的长度最大为$j-pi[j-1]-1$。那么最小前缀和后缀的数量就等于$j-j-pi[j-1]+1$,即$j-pi[j-1]$。

通过以上分析,我们可以在KMP算法中同时求解模式串的最小前缀和后缀的数量。

vector<int> prefixFunction(string s) {

int n = s.size();

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;

}

int KMP(string text, string pattern) {

int n = text.size(), m = pattern.size();

vector<int> pi = prefixFunction(pattern);

int j = 0;

for (int i = 0; i < n; i++) {

while (j > 0 && pattern[j] != text[i]) {

j = pi[j - 1];

}

if (pattern[j] == text[i]) {

j++;

}

if (j == m) {

return i - m + 1; // 匹配成功

}

}

return -1; // 匹配失败

}

int minPrefixSuffix(string s) {

int n = s.size();

vector<int> pi = prefixFunction(s);

return pi[n - 1];

}

这个算法的实现的时间复杂度是$O(m+n)$,其中$m$和$n$分别为模式串和文本串的长度。

Manacher算法

算法思想

Manacher算法是一种高效的计算最小前缀和后缀的数量的算法,因为和KMP算法一样,它可以在$O(n)$的时间复杂度内完成。

Manacher算法和前面两个算法不同的是,它利用了回文串的对称性,通过避免重复计算,进一步提高了算法的效率。

在计算最小前缀和后缀的数量的时候,Manacher算法的思路是,对于从某个点出发的回文串,在其边界和中心位置处,必然可以找到一些对称的位置,我们可以利用这些对称性,来避免重复计算。

我们遍历字符串,从第一个字符开始计算,然后依次向两侧扩展,直到回文串不能再扩展为止。我们用$r$表示当前回文串的右边界,$c$表示当前回文串的中心位置。对于当前的位置$i$,我们可以分两种情况考虑。

如果$i$在回文串中,那么我们可以利用关于$c$对称的位置$j = 2c - i$,来避免重复计算。因为$c+(c-i)$是回文串中心位置的对称位置。

如果$i$不在回文串中,那么我们需要逐一比较$i$左边和右边的字符是否相同,来寻找以$i$为中心的最长回文子串。

如此循环,我们就可以找到所有以$i$为中心的回文串。同时,我们需要维护一个以往的回文串中,最右端的位置和中心位置。在遍历过程中,我们可以在$O(1)$时间内得到以$i$为中心的最长回文串的长度。

string preprocess(string s) {

int n = s.size();

string t(2 * n + 1, '#');

for (int i = 0; i < n; i++) {

t[2 * i + 1] = s[i];

}

return t;

}

int minPrefixSuffix(string s) {

string t = preprocess(s);

int n = t.size();

vector<int> p(n, 0);

int c = 0, r = 0;

for (int i = 0; i < n; i++) {

int j = c - (i - c);

if (r > i) {

p[i] = min(r - i, p[j]);

}

while (i - p[i] - 1 >= 0 && i + p[i] + 1 < n && t[i - p[i] - 1] == t[i + p[i] + 1]) {

p[i]++;

}

if (i + p[i] > r) {

c = i;

r = i + p[i];

}

}

int ans = 0;

for (int i = 1; i < n; i += 2) {

ans = max(ans, min(p[i], (i + 1) / 2));

}

return ans;

}

这个算法的实现的时间复杂度是$O(n)$,其中$n$为字符串的长度。它是最快的计算最小前缀和后缀的数量的算法之一。

总结

以上就是几种计算最小前缀和后缀的数量的算法。它们分别是朴素算法、KMP算法和Manacher算法。我们发现,这几种算法都可以在$O(n)$的时间复杂度内完成,其中Manacher算法是最快的。

在实际应用中,我们通常使用KMP算法来进行字符串匹配,在需要计算接口哈希值等场景下,使用Manacher算法来计算最小前缀和后缀的数量。

除了这些算法,还有一些其他的算法也可以用于字符串匹配,例如Rabin-Karp算法、Boyer-Moore算法等。

对于求解字符串相关问题,我们需要不断学习和积累。希望这篇文章对您有所帮助!

后端开发标签