思路: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; } }