今天遇到了这个问题,与大家再次分享。
今天在看《算法导论》时谈到了最大子串问题,书中共提到了3种算法,时间复杂度依次是O(n^2),O(nlogn)和 O(n)。感觉挺有意思的,写出来分享一下。
定义:最大子串问题(maximum sub-array problem)
最大子串问题,又称为最大连续子串问题,是指给出一个长度为n的整数数组A,然后要求给出其中和值最大的连续子数组。也即,求出MAX(SUM(A[i...j])),其中1 ≤ i ≤ j ≤ n。
解法一:穷举(brute force)
第一种方法,也是最笨的办法,就是穷举。将所有可能的i、j组合穷举,选出其中SUM(A[i...j])最大的即可。很明显,由于可能的组合数有C(n,2)=O(n^2)个,算法的复杂度也为O(n^2)。
解法二:分治法(divide and conquer)
注意到A[1...n]的最大子串A[i..j]可能来自于以下三种情况中的一种:
- 1 ≤ i ≤ j ≤ n/2,也即最大子串出现在原串的左半部分
- n/2 ≤ i ≤ j ≤ n,也即最大子串出现在原串的右半部分
- 1 ≤ i ≤ n/2 ≤ j ≤ n,也即最大子串部分来自原串的最半部分,部分来自原串的右半部分
因此我们可以分别计算这三种情况下的解,然后将这三个解中最好的一个选出作为整个问题的解。
针对前两种情况,其实就相当于把问题的规模缩小了(divide)。把求长度为n的数组的最大子串变为求长度最n/2的数组的最大子串。这个规模变小了的问题的解同样可能有三种情况。对其中的前两种情况,我们依然采取细分的方法。可是,这样的细分会有头吗?当然会的,到了最后,我们会得到一个个长度为1的数组,在这种极限情况下,我们就不必考虑三种情况,因为此时的最大子串显然就是这个长度为1的子串自己(conquer)。
而针对第三种情况,我们采取另一种策略,即不是将问题化为相同的更小规模的问题,而是直接求解。我们首先求出A[i...n/2]使其为A[1...n/2]的最大子串,这里,进行一次遍历即可,时间复杂度为O(n)。同理,我们也可以求出A[n/2...j]使其为A[n/2...n]的最大子串。将这2个最大子串合在一起,就得到了所有会跨越串的中部的子串中的最大子串。
总结起来,假如使用T(n)代表使用此方法求解大小为n的数组的最大子串问题所消耗的时间,则有:
- T(n) = 2T(n/2) + O(n),n>1
- T(1) = O(1),n=1
不难发现,此时求解整个问题的时间复杂度为O(nlogn)。
算法的伪代码如下(截取自《算法导论》):
解法三:动态规划(dynamic programming)
时间复杂度为O(nlogn)已经是一个比较理想的结果了,但在书后的习题中还提出了另一种基于动态规划的解决思路,可以将时间复杂度降到O(n)。
具体来说,假如我们已经知道了A[1..j]这个数组的最大子串,那么A[1...(j+1)]的最大子串有可能来自于两种情况,一是其和A[1...j]的最大子串相同,二是A[1...(j+1)]的最大子串具有A[i...(j+1)]的形式,其中1 ≤ i ≤ j+1。针对第二种情况,如果我们已经知道所有尾部为j的子串中的最大子串,我们就可以在常数时间内得到所有尾部为(j+1)的子串中的最大子串。
基于上面的思想,我们只需从串的最左部开始(此时的最大子串就是串的第一个元素),向右逐渐拓展直到处理完整个串,即可得到最大子串。
算法的伪代码如下(自己写的,不一定对):
拓展:K最大子串问题(k maximum sub-array problem)
在一般的最大子串问题的基础上,我们还可以拓展出K最大子串问题,即如何求出前K个最大子串。这是另一类问题了,此处不再多叙。
参考资料:
- 《算法导论(第三版)》第四章第一节:< Introduction to Algorithms (3ed) > Chapter 4 Section 1