我刚刚写了一个问题的答案:
longest common subsequence: why is this wrong?
这个函数应该找到两个字符串之间最长的子字符串,但是当我试图找出最坏情况的运行时和导致它的输入时,我意识到我不知道。将代码视为C伪代码。
// assume the shorter string is passed in as A
int lcs(char * A, char * B)
{
int length_a = strlen(A);
int length_b = strlen(B);
// This holds the length of the longest common substring found so far
int longest_length_found = 0;
// for each character in one string (doesn't matter which), look for
// incrementally larger strings in the other
// once a longer substring can no longer be found, stop
for (int a_index = 0; a_index < length_a - longest_length_found; a_index++) {
for (int b_index = 0; b_index < length_b - longest_length_found; b_index++) {
// check the next letter until a mismatch is found or one of the strings ends.
for (int offset = 0;
A[a_index+offset] != '\0' &&
B[b_index+offset] != '\0' &&
A[a_index+offset] == B[b_index+offset];
offset++) {
longest_length_found = longest_length_found > offset ? longest_length_found : offset;
}
}
}
return longest_found_length;
}
到目前为止,这是我的想法:
下面,我假设A和B的大小大致相同,不必说A B A,我只会说n ^ 3。如果这非常糟糕,我可以更新问题。
如果代码中没有一些优化,我相信N ^ 3运行时的运行时是A B A.
但是,如果字符串不相似并且永远找不到长子字符串,那么最内层的for循环将退出到一个常数,留下A * B,对吗?
如果字符串完全相同,则算法需要线性时间,因为每个字符串只有一个同时传递。
如果字符串相似但不相同,那么longest_length_found将成为A或B中较小者的重要部分,这将分解N ^ 3中的一个因子,留下我们N ^ 2,右? 我只是想了解当它们非常相似但不完全相同时会发生什么。
大声思考,如果在第一个字母上,你会发现一个长度约为A长度一半的子串。这意味着你将运行第一个循环的A / 2次迭代,B-(A / 2)迭代第二个循环,然后在第三个循环中进行A / 2次迭代(假设字符串非常相似),而没有找到更长的子字符串。假设大致均匀的长度字符串,则为N / 2 * N / 2 * N / 2 = O(N ^ 3)。
可能显示此行为的示例字符串:
A A A B A A A B A A A B A A A B
A A A A B A A A A B A A A A B A
我是关闭还是我遗漏了某些东西或误用了某些东西?
我很确定我可以使用trie /前缀树做得更好,但同样,我真的很想了解这个特定代码的行为。
答案 0 :(得分:1)
我认为评论中所说的 roliu 是对钱的抨击。我认为你的算法是 O(N 3 ),最好的情况是 O(N 2 )。
我实际想要指出的是这种算法的过度放纵。您会看到,对于每个字符串中的每个可能的起始偏移量,您将测试每个后续匹配字符以计算匹配数。但请考虑这样的事情:
A = "01111111"
B = "11111110"
您将发现的第一件事是从A[1]
和B[0]
开始的最大匹配子字符串,然后您将测试从A[2]
开始的那些完全重叠的部分, B[1]
等等......这里重要的是相对偏移量。通过实现这一点,您可以完全删除算法的 N 3 部分。然后它变成移动其中一个阵列在另一个下面的问题。
A 01111111
B 11111110
B 11111110
B 11111110
B ... -->
B 11111110
为了使代码不那么复杂,你可以测试一半的系统,然后交换数组并测试另一半:
// Shift B under A
A 01111111
B 11111110
B ... -->
B 11111110
// Shift A under B
B 11111110
A 01111111
A ... -->
A 01111111
如果你这样做,那么你就有 O((A + B-2)* min(A,B)/ 2),或更方便 O(N < SUP> 2 )强>
int lcs_half(char * A, char * B)
{
int maxlen = 0, len = 0;
int offset, i;
for( offset = 0; B[offset]; offset++ )
{
len = 0;
for( i = 0; A[i] && B[i+offset]; i++ )
{
if( A[i] == B[i+offset] ) {
len++;
if( len > maxlen ) maxlen = len;
}
else len = 0;
}
}
return maxlen;
}
int lcs(char * A, char * B)
{
int run1 = lcs_half(A,B);
int run2 = lcs_half(B,A);
return run1 > run2 ? run1 : run2;
}
答案 1 :(得分:1)
因此,在我们在评论中讨论它之后,我们同意问题是找到代码的最坏情况运行时。我们可以使用以下证明至少Omega(n^3)
声明它:
让
A = aaaa...aabb...bbbb
表示|A| = n
,它由n/2
a
&{39}和n/2
b
&#39;组成。秒。
B = aaaa....
其中|B| = n
。
现在我们考虑最外层循环的第一次n/2
次迭代(即n/2
字符串的第一个A
起始索引。修复最外层循环的第一次i
次迭代的一些迭代n/2
。第二个循环的上限是至少 n-n/2 = n/2
,因为两个字符串的LCS长度为n/2
。对于第二个循环的每次迭代,我们匹配一个长度为n/2 - i
的字符串(您可以通过矛盾证明这一点)。所以我们在最外层循环的第一次n/2
次迭代之后得到了这一行:
longest_length_found = longest_length_found > offset ? longest_length_found : offset;
已经运行:
n/2*(n/2) + n/2*(n/2-1) + n/2*(n/2-2) + ... + n/2*(2) + n/2*(1) = n/2*Omega(n^2) = Omega(n^3)
具体来说,对于最外层循环的第一次迭代,我们在字符串n/2
中有a
A
&#39;并且n/2
B
中的起始点。对于B
中的每个起始点,我们将匹配长度为n/2
的完整公共子字符串(意味着我们将点击该行n/2
次)。这就是n/2*(n/2)
。对于最外层循环的下一次迭代,我们在字符串n/2-1
中有一个a
A
字符串,n/2
中仍有B
个起始点{1}}。在这种情况下,我们为每个起始索引n/2-1
匹配长度为=> n/2(n/2-1)
的公共子字符串。同样的论证归纳为i = n/2
。
无论如何,我们知道算法在输入上的运行时间比最外层循环的第一次n/2
次迭代的运行时间长,所以它也是Omega(n^3)