好的,所以这个问题肯定可以在 O(n) 内解决,我们只需要按照你的建议巧妙地使用 KMP。
解决最长的正确前缀(也是后缀问题)是我们将使用的 KMP 的重要部分。
最长的正确前缀也是一个后缀问题,所以我们暂时称它为前缀后缀问题。
前缀后缀问题可能很难理解,所以我将包含一些示例。
“abcabc”的前缀后缀解决方案是“abc”,因为它是最长的字符串,既是正确的前缀又是正确的后缀(正确的前缀和后缀不能是整个字符串)。
“abcabca”的前缀后缀解法是“a”
Hmmmmmmmmm等一下,如果我们只是从“abcabca”的末尾切掉“a”,我们就剩下“abcabc”,如果我们得到这个新字符串的解决方案(“abc”)并再次切掉它,我们就剩下了“abc”嗯嗯嗯嗯嗯。非常有趣。(这几乎是解决方案,但我会谈谈为什么会这样)
好吧,让我们尝试将这种直觉形式化一点,看看我们是否可以找到解决方案。
我将在我的论点中使用一个关键假设:
我们模式的最小周期是我们模式中每个较大周期的有效周期
让我们将i
模式的第一个字符的前缀后缀解决方案存储在lps[i]
. 该lps
数组可以计算并用于 KMP 算法,您可以在此处O(n)
阅读有关如何计算它的更多信息: https ://www.geeksforgeeks.org/kmp-algorithm-for-pattern-searching/O(n)
为了让我们清楚,我将列出一些lps
数组的一些示例
模式:“aaaa”
lps: [0, 1, 2, 3, 4]
模式:“aabbcc”
lps: [0, 1, 0, 0, 0, 0]
模式:“abcabcabc”
lps: [0, 0, 0, 1, 2, 3, 4, 5, 6]
好的,现在让我们定义一些变量,以帮助我们找出为什么这个lps
数组有用。
让l
我们的模式的长度,让我们的 lps 数组( )k
中的最后一个值k=lps[l-1]
该值k
告诉我们k
字符串的第一个字符与字符串的最后一个字符相同k
。我们可以利用这个事实来找到一个时期!
使用此信息,我们现在可以证明由l-k
字符串的第一个字符组成的前缀形成了一个有效句点。这很清楚,因为我们定义数组的方式k
,不在我们前缀中的下一个字符必须匹配我们前缀的第一个字符。我们前缀中的第一个字符必须与构成我们后缀的最后一个字符相同。k
lps
k
k
在实践中,您可以使用一个简单的 while 循环来实现这一点,如下所示,其中index
标记您当前考虑的后缀的结尾是最小的周期。
public static void main(String[] args){
String pattern="abcabcabcabca";
int[] lps= calculateLPS(pattern);
//start at the end of the string
int index=lps.length-1;
while(lps[index]!=0){
//shift back
index-=lps[index];
}
System.out.println(pattern.substring(0,index+1));
}
而且由于计算lps
发生在 中O(n)
,并且您总是在 while 循环中向后移动至少 1 步,因此整个过程的时间复杂度很简单O(n)
如果您想查看我的确切代码,我在我的 calculateLPS() 方法中大量借鉴了 KMP 的 geeksForGeeks 实现,但我建议您也查看他们的解释:https ://www.geeksforgeeks.org/kmp -用于模式搜索的算法/
static int[] calculateLPS(String pat) {
int[] lps = new int[pat.length()];
int len = 0;
int i = 1;
lps[0] = 0;
while (i < pat.length()) {
if (pat.charAt(i) == pat.charAt(len)) {
len++;
lps[i] = len;
i++;
}
else {
if (len != 0) {
len = lps[len - 1];
}
else {
lps[i] = len;
i++;
}
}
}
System.out.println(Arrays.toString(lps));
return lps;
}
最后但同样重要的是,感谢您发布如此有趣的问题,弄清楚这很有趣!我也是新手,所以如果我的解释的任何部分没有意义,请告诉我。