https://oj.leetcode.com/problems/maximum-subarray/
Find the contiguous subarray within an array (containing at least one number) which has the largest sum.
For example, given the array [−2,1,−3,4,−1,2,1,−5,4]
,
the contiguous subarray [4,−1,2,1]
has the largest sum = 6
.
public class Solution { public int maxSubArray(int[] A) { int max = A[0]; int sum = 0; if(A.length == 1){ return A[0]; } for(int i = 0; i < A.length; i ++){ sum += A[i]; if(max < sum){ max = sum; } if(sum < 0){ sum = 0; } } return max; } }
解题思路:
考虑一个数组,前面一段总和<0的子数组,一定是要丢弃的,因为会影响后面的总和。所以就从头遍历这个数组,sum初始化为0的值,max也初始化为A[0]。不断给sum加上A[i],同时与max进行比较,大于max则替换max。如果sum一旦<0,丢弃。所以这时候将sum置为0,从i开始从新计算。但是这是max已经存有前面已经丢弃数组中的最大和了,并留着与后面的进行对比。
解析:
与暴力循环两次相比,(i:0-1,j:0-1不断求和,最后比较)。该方法的关键在于理解,一个总和<0的数组,是无法被后面的数字所用的,必然会减小总和。所以这时可以直接从i处开始新的数组进行计算。但同时要注意,并不意味着前面被丢弃的数组内就没有最大和,所以仍需保存下来,继续比较。
因为该算法用到了“最佳子结构”(以每个位置为终点的最大子数列都是基于其前一位置的最大子数列计算得出),该算法可看成动态规划的一个例子。从动态规划的角度看,Base case: 我们首先建立一个与原数列A长度相同的数列dp,dp中的每一个值dp[i]表示以位置i为终点的子数列的最大和。Induction case: 如果以前面一个位置为结束的子数列和为正数,既dp[i-1]>0,那么dp[i]=dp[i-1]+A[i];否则dp[i]=A[i]。那么dp[n]就是我们想要的该数列的最大连续子数列之和。算法代码如下:
public int max_subarray(int[] A){ int[] dp = new int[A.length]; for(int i=0;i<A.length;i++){ dp[i]=A[i]; } int max=A[0]; for(int i=1;i<A.length;i++){ if(dp[i-1]>0){ dp[i]=dp[i-1]+A[i]; }else{ dp[i]=A[i] } max=Math.max(max,dp[i]); } return max; }
注意点:
数组可能都是负数,所以不能把max的值初始化为0.
这个问题还有一个被称为Kadane's algorithm的解法。如下。思路是,考虑已经知道以A[i -1]结尾的各个子数列最大和为max[i-1],如何求以A[i]结尾的子数列和max[i]?无非两种可能,1.max[i - 1] + A[i]。2.A[i]。然后再将该值与存有的最大和进行比较,如此往复。实质也是,如果max[i-1]<0,就丢弃,直接取A[i],否则就加上A[i]。
public static int maxSubArray(int[] A) { int maxSoFar=A[0], maxEndingHere=A[0]; for (int i=1;i<A.length;++i){ maxEndingHere= Math.max(maxEndingHere+A[i],A[i]); maxSoFar=Math.max(maxSoFar, maxEndingHere); } return maxSoFar; }
此外,还有分治的解法,就较为复杂了。
public class Solution { public int maxSubArray(int[] A) { return calcMax(A, 0, A.length - 1, Integer.MIN_VALUE); } public int calcMax (int[] A, int left, int right, int previousMax){ if(left > right){ return previousMax; } int mid = (left + right) / 2; //int nowMax = previousMax; //最大串在左侧的情况 int leftMax = calcMax(A, left, mid - 1, previousMax); if(leftMax > previousMax){ previousMax = leftMax; } //最大串在右侧的情况 int rightMax = calcMax(A, mid + 1, right, previousMax); if(rightMax > previousMax){ previousMax = rightMax; } //最大串跨越中间的情况 int maxLeft = 0; int maxRight = 0; int sum = 0; for(int i = mid - 1; i >= left; i--){ sum += A[i]; if(sum > maxLeft){ maxLeft = sum; } } sum = 0; for(int i = mid + 1; i <= right; i++){ sum += A[i]; if(sum > maxRight){ maxRight = sum; } } int maxMid = maxLeft + maxRight + A[mid]; if(maxMid > previousMax){ previousMax = maxMid; } return previousMax; } }
这里注意,
//最大串跨越中间的情况 int maxLeft = 0; int maxRight = 0;
这里必须定义为0,而不能是Integer.MIN_VALUE。因为真正的maxMid在maxMid = maxLeft + maxRight + A[mid],这一句定义的。
也可以将其定义改为如下,从mid往两侧计算,都把A[mid]包含在内。
//最大串跨越中间的情况 int maxLeft = A[mid]; int maxRight = A[mid]; int sum = A[mid];
最后maxMid = maxLeft + maxRight - A[mid];再减去A[mid]。完整代码如下。
public class Solution { public int maxSubArray(int[] A) { return calcMax(A, 0, A.length - 1, Integer.MIN_VALUE); } public int calcMax (int[] A, int left, int right, int previousMax){ if(left > right){ return previousMax; } int mid = (left + right) / 2; //int nowMax = previousMax; //最大串在左侧的情况 int leftMax = calcMax(A, left, mid - 1, previousMax); if(leftMax > previousMax){ previousMax = leftMax; } //最大串在右侧的情况 int rightMax = calcMax(A, mid + 1, right, previousMax); if(rightMax > previousMax){ previousMax = rightMax; } //最大串跨越中间的情况 int maxLeft = A[mid]; int maxRight = A[mid]; int sum = A[mid]; for(int i = mid - 1; i >= left; i--){ sum += A[i]; if(sum > maxLeft){ maxLeft = sum; } } sum = A[mid]; for(int i = mid + 1; i <= right; i++){ sum += A[i]; if(sum > maxRight){ maxRight = sum; } } int maxMid = maxLeft + maxRight - A[mid]; if(maxMid > previousMax){ previousMax = maxMid; } return previousMax; } }
另外,再上述解法中,每次递归传入的previousMax可以不传入。(思考一下为什么?)代码如下:
public class Solution { public int maxSubArray(int[] A) { return calcMax(A, 0, A.length - 1); } public int calcMax (int[] A, int left, int right){
if(left > right){ return Integer.MIN_VALUE; } int mid = (left + right) / 2; //最大串在左侧的情况 int leftMax = calcMax(A, left, mid - 1); //最大串在右侧的情况 int rightMax = calcMax(A, mid + 1, right); //最大串跨越中间的情况 int maxLeft = A[mid]; int maxRight = A[mid]; int sum = A[mid]; for(int i = mid - 1; i >= left; i--){ sum += A[i]; if(sum > maxLeft){ maxLeft = sum; } } sum = A[mid]; for(int i = mid + 1; i <= right; i++){ sum += A[i]; if(sum > maxRight){ maxRight = sum; } } int midMax = maxLeft + maxRight - A[mid]; return Math.max(Math.max(leftMax, rightMax), midMax); } }
综上,至少有五种思考的思路。
1. 循环两次暴力求解。O(n^2)
2. 我的解法。O(n)。实质就是动态规划。
3. 动态规划。O(n)。
4. 分治。
5. Kadane的解法。其实和我的解法(动态规划)相同。
所以解法有三种。(暴力遍历,动态规划,分治)