53-最大子序和(动态规划与分治法)
思路:https://leetcode-cn.com/problems/maximum-subarray/solution/zheng-li-yi-xia-kan-de-dong-de-da-an-by-lizhiqiang/
思路一:分治法
分治法基本思路:
1.分解:把原问题分解成若干个大小相近,互相独立,无公共子问题的子问题。
2.解决:求解子问题。
3.合并:组合子问题的解得到原问题的解。
分治法是将整个数组切分成几个小组,然后每个小组再切分成几个更小的小组,一直到不能继续切分也就是只剩一个数字为止。每个小组会计算出最优值,汇报给上一级的小组,一级一级汇报,上级拿到下级的汇报找到最大值,得到最终的结果。和归并排序的算法类似,先切分,再合并结果。
这个问题中的关键就是如何切分这些组合才能使每个小组之间不会有重复的组合(有重复的组合意味着有重复的计算量),这个问题应该困扰了不少的同学,我在学习理解的时候也花了不少时间。
首先是切分分组方法,就这个案例中的例子来,我们有一个数组 [-2,1,-3,4,-1,2,1,-5,4] ,一共有 9 个元素,我们 center=(start + end) / 2 这个原则,得到中间元素的索引为 4 ,也就是 -1,拆分成三个组合:
(1)[-2,1,-3,4,-1]以及它的子序列(在-1左边的并且包含它的为一组)
(2)[2,1,-5,4]以及它的子序列(在-1右边不包含它的为一组)
(3)任何包含-1以及它右边元素2的序列为一组(换言之就是包含左边序列的最右边元素以及右边序列最左边元素的序列,比如 [4,-1,2,1],这样就保证这个组合里面的任何序列都不会和上面两个重复)
以上的三个组合内的序列没有任何的重复的部分,而且一起构成所有子序列的全集,计算出这个三个子集合的最大值,然后取其中的最大值,就是这个问题的答案了。
然而前两个子组合可以用递归来解决,一个函数就搞定,第三个跨中心的组合应该怎么计算最大值呢?
答案就是先计算左边序列里面的包含最右边元素的子序列的最大值,也就是从左边序列的最右边元素向左一个一个累加起来,找出累加过程中每次累加的最大值,就是左边序列的最大值。
同理找出右边序列的最大值,就得到了右边子序列的最大值。左右两边的最大值相加,就是包含这两个元素的子序列的最大值。
在计算过程中,累加和比较的过程是关键操作,一个长度为 n 的数组在递归的每一层都会进行 n 次操作,分治法的递归层级在 logNlogN 级别,所以整体的时间复杂度是 O(nlogn)O(nlogn),在时间效率上不如动态规划的 O(n)O(n) 复杂度。
分治法的思路是这样的,其实也是分类讨论。
连续子序列的最大和主要由这三部分子区间里元素的最大和得到:
第 1 部分:子区间 [left, mid];
第 2 部分:子区间 [mid + 1, right];
第 3 部分:包含子区间[mid , mid + 1]的子区间,即 nums[mid] 与nums[mid + 1]一定会被选取。
对它们三者求最大值即可。
public int maxSubArray(int[] nums) { return maxSubArrayDivideWithBorder(nums, 0, nums.length-1); } private int maxSubArrayDivideWithBorder(int[] nums, int start, int end) { if (start == end) { // 只有一个元素,也就是递归的结束情况 return nums[start]; } // 计算中间值 int center = (start + end) / 2; int leftMax = maxSubArrayDivideWithBorder(nums, start, center); // 计算左侧子序列最大值 int rightMax = maxSubArrayDivideWithBorder(nums, center + 1, end); // 计算右侧子序列最大值 // 下面计算横跨两个子序列的最大值 // 计算包含左侧子序列最后一个元素的子序列最大值 int leftCrossMax = Integer.MIN_VALUE; // 初始化一个值 int leftCrossSum = 0; for (int i = center ; i >= start ; i --) { leftCrossSum += nums[i]; leftCrossMax = Math.max(leftCrossSum, leftCrossMax); } // 计算包含右侧子序列最后一个元素的子序列最大值 int rightCrossMax = nums[center+1]; int rightCrossSum = 0; for (int i = center + 1; i <= end ; i ++) { rightCrossSum += nums[i]; rightCrossMax = Math.max(rightCrossSum, rightCrossMax); } // 计算跨中心的子序列的最大值 int crossMax = leftCrossMax + rightCrossMax; // 比较三者,返回最大值 return Math.max(crossMax, Math.max(leftMax, rightMax)); } 作者:lizhiqiang-3 链接:https://leetcode-cn.com/problems/maximum-subarray/solution/zheng-li-yi-xia-kan-de-dong-de-da-an-by-lizhiqiang/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
思路二:动态规划
求解对象:最优化问题(最大最小值)
动态规划总体思路:
与分治法类似,把问题分为规模逐渐减小的子问题,不过,子问题是互相重复的。
用例:
[-2,1,3,4,-1,2,1,-5,4],
i),如果有n个数字,时间复杂度是$O(n^2) $,这样的时间复杂度是明显不能接受的。
我们把目光落到动态规划上面来,首先需要把这个问题分解成最优子问题来解。最主要的思路就是将上面的45个组合进行
,分解成数量较少的几个子问题。在这里我们一共有9个数字,顺理成章的我们把组合分解成9个小组的组合。
第一个子组合是以第一个数字结尾的连续序列,也就是 [-2],最大值-2
第二个子组合是以第二个数字结尾的连续序列,也就是 [-2,1], [1],最大值1
第三个子组合是以第三个数字结尾的连续序列,也就是 [-2,1,3], [1,3], [3],最大值4
以此类推。。。
如果我们能够得到每一个子组合的最优解,也就是子序列的最大值,整体的最大值就可以通过比较这9个子组合的最大值来得到了。现在我们找到了最优子问题,重叠子问题在哪呢?那就得细心比较一下每个子问题。
从第二个子组合和第三个子组合可以看到,组合 3 只是在组合 2 的基础上每一个数组后面添加第 3 个数字,也就是数字 3,然后增加一个只有第三个数字的数组 [3] 。这样两个组合之间的关系就出现了,可是我们不关心这个序列是怎么生成的,只是关心最大值之间的关系。
下面我们看组合 3 的组成,我们将子组合 3 分成两种情况:
1.继承子组合二得到的序列,也就是[-2,1,3], [1,3] (最大值 1 = 第二个组合的最大值 + 第三个数字)
2、单独第三个数字的序列,也就是[3] (最大值 2 = 第三个数字)
如果 第二个子组合(只有前两个数时)的最大值 大于0,而且已知第三个数字大于零,那么最大值 1 就比最大值 2 要大;如果前两个数的和小于零,那最大值一定不包括前两个数。这样,我们就通过第二个组合的最大值和第三个数字,就得到了第三个组合的最大值。因为第二个组合的结果被重复用到了,所以符合这个重叠子问题的定义。通俗来讲这个问题就变成了,第 i 个子组合的最大值可以通过第i-1个子组合的最大值和第 i 个数字获得,如果第 i-1 个子组合的最大值没法给第 i 个数字带来正增益,我们就抛弃掉前面的子组合,自己就是最大的了。
class Solution { public int maxSubArray(int[] nums) { int sum=Integer.MIN_VALUE;//动态规划 int b=0;//当前的最大字段和 for(int i=0;i<nums.length;i++) { if(b<=0)b=nums[i];//当前的最大字段和小于零,那就把b赋值成当前元素 else b+=nums[i];//当前最大字段和大于零,就加上当前元素 if(b>sum) sum=b; } return sum; } }