最大子序列求和是指给定一组序列,求所有连续子序列的和中的最大值,例如给定数列:
[5,-2,-5,6]最大子序列和是6;[1, 2, -3, 4, -5, 6, 7, 8, -9, 10]最大子序列和是22;
下面将利用几种不同的算法来解决此问题,重要的是理解不同算法中所代表的思想
1、穷举法
穷举法的思想比较简单,它是指列举所有的可能,来得到问题最终的解;
在此问题中,可以利用穷举法将所有的子序列的和计算出来,来得到最大子序列的和;
假设子序列的起点为i,那么i的范围在数组下标中可以是:[0,arrays.length-1];
针对起点为i的子序列,由于子序列是连续的,那么它的终点的范围是[i,arrays.length-1];
最后我们需要对[i,j]的子序列进行求和,并把结果每次与max比较,以此得到最大子序列和max;
//穷举法,时间复杂度为O(N^3)
public static int method_1(int[] arrays) {
int max = 0;
//每个子序列的起点 = i
for (int i = 0; i < arrays.length; i++) {
//每个子序列的终点 = j
for (int j = i; j < arrays.length; j++) {
int sum = 0;
//子序列求和
for (int k = i; k <= j; k++) {
sum = sum + arrays[k];
}
//当出现子序列和大于max,用sum替换掉max
if (sum > max) {
max = sum;
}
}
}
return max;
}
2、穷举优化
显然,上面算法的时间复杂度O(N^3)并不能让人满意,不过我们可以简单优化一下:
列举所有的子序列[i,j]依然不变,但是针对子序列[i,j]求和,我们完全可以省略这一步,当固定起点i时,以i为起点的子序列的终点j的范围[i,arrays.length-1]是连续的,可以发现,j=i+1为子序列的终点时,它的和为:
SUM(i,j)=SUM(i,i+1)=SUM(i,i)+arrays[i+1];
同理,j=i+2时:
SUM(i,j)=SUM(i,i+2)=SUM(i,i+1)+arrays[i+2]
……
也就是说,我们可以把上一次子序列的求和保存起来,留待下次j递增(j++)后使用,即不用针对每次子序列[i,j],去重新计算它的和,在代码里,只需要把对sum的初始化int sum = 0 提到上一层循环里就可以了,并去掉重复计算的for循环就可以了,此方法的时间复杂度为O(N^2)
//穷举优化:时间复杂度为O(N^2) public static int method_2(int[] arrays) { int max = 0; //每个子序列的起点 = i for (int i = 0; i < arrays.length; i++) { int sum = 0; //每个子序列的终点 = j for (int j = i; j < arrays.length; j++) { sum = sum + arrays[j]; //当出现子序列和大于max,用sum替换掉max if (sum > max) { max = sum; } } } return max; }
这里稍微有点动态规划的思想,但并不完全,下面我们将介绍基于动态规划的思想的Kadane算法对此问题的解法思路
3、Kadane算法-动态规划
穷举法是针对具体的子序列[i,j]去求解,虽然做了优化减少了重复计算,但依然需要比较高的时间复杂度。
最大子序列和的最终答案是值,而不用去求具体的子序列,所以这里我们可以巧妙的运用动态规划的思想来解决,动态规划的核心思想是:拆分成若干子问题,记住过往,减少重复计算。
假设我们求长度为N的序列的最大子序列和,可以拆分成N个子问题来计算,假设此数组序列下标是i,那么这N个子问题分别是:i=[0]、i=[0,1]、i=[0,1,2]、......、i=[0,1,2,...,N-1]的子序列的最大子序列和,我们这里可以不用逆推,直接采用顺推的方式来实现。
我们可以根据i=[0]的子序列的结果,去推算i=[0,1]的结果,然后用i=[0,1]的子序列的结果去推算i=[0,1,2]的结果,以此类推,最终推算出i=[0,1,2,...,N-1]的结果;
然而值得我们注意的是,我们需要知道使用前一个子问题去推算后一个子问题,它们之间的连接关系:
当 i(k)=[0,1,2,3,...k] --> i(k+1)=[0,1,2,3,...k,k+1],即用i(k)结果去推算i(k+1)的结果的时候:
我们定义两个变量max,sum;max(k)代表i(k)的结果(即最大子序列和),sum(k)代表
i(k)序列的累加,(max ≠ sum);
sum(k+1) = sum(k) + arrays[k+1]
当sum(k+1) > max(k) 时,则 max(k+1) = sum(k+1)
否则max(k+1)=max(k);
要使上述成立我们必须所做的一个操作是,当sum < 0时,需要把sum = 0(结合下面i = 4/5时更容易理解)
例如对于数组[1, 2, -3, 4, -5, 6, 7, 8, -9, 10],
Arrays 1 2 -3 4 -5 6 7 8 -9 10 i(下标) 0 1 2 3 4 5 6 7 8 9 详解,当遍历到:
i=0时:
sum=1 ----> sum>max ----> max = 0 : 序列[1]的最大子序列和为 1
i=1时:
sum = 3 ----> sum>max ----> max = 3 : 序列[1,2]的最大子序列和为3
i=2时:
sum=0 ----> sum<max ----> max = 3 : 序列[1,2,-3]的最大子序列和为3
i=3时:
sum=4 ----> sum>max ----> max = 4 : 代表序列[1,2,-3,4]的最大子序列和为4
(注意:此时的和为最大的值的子序列有两个[1,2,-3,4],[4],但我们并不在意,我们需要在意的是状态(sum是否小于0)以及max(sum是否大于max),sum如果一直没有小于0,那么sum的值对于后面的累加都是有效的增大,而sum只要一小于0,我们需要把sum=0,它才不影响后面最大值的累加。强调一下:我们只关注当前所遍历过的这整个一块的最大子序列的和max以及sum值的状态)
i=4时:
sum=-1 ----> sum<max ----> max = 4 ---->sum=0:序列[1,2,-3,4,-5]的最大子序列和为4
i=5时:
sum=6 ----> sum>max ----> max = 6 : 序列[1,2,-3,4,-5,6]的最大子序列和为6
......
i=8时:
sum=6+7+8-9 ----> sum<max(6+7+8) ----> max=21 : 序列[1, 2, -3, 4, -5, 6, 7, 8, -9]的最大子序列和为21
i=9时:
sum=6+7+8-9+10=22 ----> sum > max ----> max = 22 :序列[1, 2, -3, 4, -5, 6, 7, 8, -9, 10]的最大子序列和为22。
可以清晰的看到,我们只需要一次遍历,就可以得到最终的解,时间复杂度为O(N);
代码如下:
//动态规划,时间复杂度O(N) public static int method_3(int[] arrays) { int max = 0; int sum = 0; for (int i = 0; i < arrays.length; i++) { sum = sum + arrays[i]; if (sum > max) { max = sum; }else if(sum < 0){ sum = 0; } } return max; }
4、分治策略
分治策略的核心思想为:分而治之。
它跟动态规划相同点:都是将问题分成若干个子问题。
不同点:动态规划的场景一般为:每个子问题与前后都是有联系的,以1推2,以2推3,依次推到最终结果;而分治策略的场景一般为:先独立的计算出每个子问题的结果,再合并结果得到最终结果。
左半部分 起点 右半部分 1 2 -3 4 -5 6 7 8 -9 10 在此问题中,我们也同样可以使用分治策略来解决:我们很容易知道最大子序列和可能在三处出现,如:
A、左半部分:
B、右半部分:
C、中间部分(左右都占):
AB两种情况我们可以通过递归求解;C情况我们可以算出以左半部分的起点向左依次遍历的最大和值,以右半部分的起点向右依次遍历得出的最大和值,两者相加;最终我们比较ABC三者的最大值,即可返回最终结果。
此种方法的时间复杂度为O(N*logN),想比较动态规划而言,它有些地方重复计算了,但是如果将问题转换为求最大子序列的起点跟终点,动态规划或许不太适用了。
//分治策略,时间复杂度为O(N*logN) public static int method_4(int[] arrays,int left,int right) { //基准情况 if(left == right){ if(arrays[left] > 0){ return arrays[left]; }else{ return 0; } } int center = (left+right)/2; //递归求左半部分(A情况) int maxLeft = method_4(arrays,left,center); //递归求右半部分(B情况) int maxRight = method_4(arrays,center+1,right); //求C情况的以左半部分起点向左遍历的最大值 int maxLeftBorder = 0; int sumLeftBorder = 0; for(int i = center;i>=left;i--){ sumLeftBorder = sumLeftBorder+arrays[i]; if(sumLeftBorder>maxLeftBorder){ maxLeftBorder = sumLeftBorder; } } //求C情况的以右半部分起点向右遍历的最大值 int maxRightBorder = 0; int sumRightBorder = 0; for(int i = center+1;i<=right;i++){ sumRightBorder = sumRightBorder+arrays[i]; if(sumRightBorder>maxRightBorder){ maxRightBorder = sumRightBorder; } } //返回 maxLeft 、maxRight、maxLeftBorder+maxRightBorder中的最大值 int temp = Math.max(maxLeft, maxRight); return Math.max(temp,maxLeftBorder+maxRightBorder); }