CLRS最大子数组问题
今天我们一起来看一下关于最大子数组的一些问题。最大子数组的应用场景可以是这样的:有一天,你搞了一场投资开始炒股,这时你就会想,我怎样才能获得最大的利润呢,最简单的想法就是我在股票的最低价时买入,然后在最高价时卖出,这样利润必然最大。但冷静下来想想这往往是不可能的,你不能保证最高价出现在最低价后面。为了达到这一目的,我们建立了最大子数组模型。我们以一年为时间期限,每个月的股价假设是这样的13 12 15 18 19 18 20 16 13 9 11 10,为了获得最大利益,我们要寻找差值最大的两个数(后一个减前一个)。为了简化模型,我们从另外一个角度来看待这个问题。当我们从数据的变化(也就是从后一个月相对于前一个月价格的变化量)角度来看待这个问题时,这一串数字就变成了这个样子0 -1 3 3 1 -1 2 -4 -3 -4 2 -1,这样问题就转化为寻找这个数组的最大非空连续子数组问题了。 下面我们就来讨论一下这个问题的解法:
解法一:暴力求解
最简单最直接的想法就是我求出所有非空连续子数组的和,再通过比较找到最大的一个子数组不就行了吗。确实可以,通过for循环遍历我们可以得到这样的伪代码:
暴力求解伪代码:
FindMaxSubarray(A,low,high)
1. maxSum = 负无穷大
2. sum = 0
3. for i = 1 to n
4. for j = i to n
5. sum=sum+A[j]
6. if sum > maxSum
7. maxSum = sum
8. maxLeft = i
9. maxRight = j
10. sum = 0
11. return (maxLeft,maxRight,maxSum)
伪代码讲解:
第一二行,maxSum用来表示当前扫描过的最大子数组的和,sum用来表示当前子数组的和
第三到十行,主体扫描所有子数组并求和,记录最大子数组的起始位置i、结束位置j以及和sum
第十一行返回扫描结果
C语言完整代码:
/*Author: Terry Zhang*/ #include <stdio.h> #include <stdlib.h> struct info{ int maxLeft; int maxRight; int Sum; }; int main() { size_t n; scanf_s("%d", &n); int *p = (int *)calloc(n, sizeof(int)); for (size_t i = 0; i < n; i++) { scanf_s("%d", p + i); } info Find_Max_Subarray(int A[], int low, int high); info inf; inf = Find_Max_Subarray(p, 0, n - 1); //输出结果 printf("MaxLeft:%d\n", inf.maxLeft+1); //默认起始位置为1 printf("MaxRight:%d\n", inf.maxRight+1); printf("MaxSum:%d\n", inf.Sum); if (p != NULL) free(p); return 0; } info Find_Max_Subarray(int A[], int low, int high) { int maxSum = INT_MIN; int maxLeft = 0; int maxRight = 0; int sum = 0; for (size_t i = low; i <= high; i++) { for (size_t j = i; j < high; j++) { sum += A[j]; if (sum >= maxSum) { maxSum = sum; maxLeft = i; maxRight = j; } } sum = 0; } info inf; inf.maxLeft = maxLeft; inf.maxRight = maxRight; inf.Sum = sum; return inf; }
解法二:使用分治策略求解
使用分治策略意味着我们要把数组划分成两个规模尽量相等的子数组。以mid为划分点,则数组A[low,high]的任何连续子数组A[i,j]的位置必然有一下三种情况。
1. 完全位于子数组A[low,mid]中,此时low<=i<=j<=mid
2. 完全位于子数组A[mid,high]中,此时mid<=i<=j<=high
3. 跨越中点mid,此时low<=i<=mid<j<=high
我们可以递归求解第一二种情况,因为它们仍然是最大子数组问题,只是规模更小。现在解决问题的重点落在了解决第三种情况,那就是子数组跨越中点情况。而仔细思考后我们发现第三种情况可以这样去处理,我们可以找到形如A[i,mid]和A[mid+1,j]的最大子数组,然后我们将两个子数组合并来得到A[low,high]跨越中点的“最大子数组”,当然这个子数组只是跨越中点的最大子数组,我们还需要将其和其它两种情况得到的最大子数组比较从而确定整个数组的最大子数组。
找到跨越中点的“最大子数组”的伪代码:
FindMaxCrossingSubarray(A,low,mid,high)
1. leftSum = 负无穷大
2. sum = 0
3. for i=mid downto low
4. sum = sum + A[i];
5. if sum > leftSum
6. leftSum = sum
7. maxLeft = i
8. sum = 0
9. rightSum = 负无穷大
10. for j=mid+1 to high
11. sum = sum + A[j]
12. if sum > rightSum
13. rightSum = sum
14. maxRight = j
15. return (maxLeft,maxRight,leftSum+rightSum)
伪代码讲解:
第一到七行找出A[low,mid]的最大子数组,并记录子数组最左侧的边界位置及子数组和
第八到十四行找出A[mid+1,high]的最大子数组,并记录子数组最右侧的边界位置及子数组的和
第十五行返回执行结果
FindMaxCrossingSubarray(A,low,mid,high)完整C代码如下:
info Find_Max_Crossing_Subarray(int A[], int low, int mid, int high) { int leftSum = INT_MIN; info p; int sum = 0; for (int i = mid; i >= low; i--) { sum = sum + A[i]; if (sum > leftSum) { leftSum = sum; p.maxLeft = i; } } sum = 0; int rightSum = INT_MIN; for (int j = mid + 1; j <= high; j++) { sum = sum + A[j]; if (sum > rightSum) { rightSum = sum; p.maxRight = j; } } p.Sum = leftSum + rightSum; return p; }
解决了问题的重头戏,我们也就很容易得到FindMaxSubarray的伪代码了。
求解最大子数组伪代码:
FindMaxSubarray(A,low,high)
1. if low == high
2. return (low,high,A[low])
3. else mid = (low+high)/2 向下取整
4. (leftLow,leftHigh,leftSum) = FindMaxSubarray(A,low,mid)
5. (rightLow,rightHigh,rightSum) = FindMaxSubarray(A,mid+1,high)
6. (crossLow,crossHigh,crossSum) = FindMaxCrossingSubarray(A,low,mid,high)
7. if leftSum >= rightSum and leftSum >= crossSum
8. return (leftLow,leftHigh,leftSum)
9. else if rightSum >= leftSum and rightSum >= crossSum
10. return (rightLow,rightHigh,rightSum)
11. else return (crossLow,crossHigh,crossSum)
这样求解最大子数组问题我们只需要初始调用A[A,1,A.Length]即可。
完整C代码如下:
info Find_Max_Subarray(int A[], int low, int high) { info pLeft, pRight, pCross; if (low == high) { pLeft.maxLeft = low; pLeft.maxRight = high; pLeft.Sum = A[low]; return pLeft; } else { int mid = (low + high) / 2; pLeft = Find_Max_Subarray(A, low, mid); pRight = Find_Max_Subarray(A, mid + 1, high); pCross = Find_Max_Crossing_Subarray(A, low, mid, high); if (pLeft.Sum >= pRight.Sum && pLeft.Sum >= pCross.Sum) { return pLeft; } else if (pRight.Sum >= pLeft.Sum && pRight.Sum >= pCross.Sum) { return pRight; } else return pCross; } }
解法三:非递归、线性时间解法
有没有更好的算法呢。我们可以这样思考,从数组的左边界开始,由左到右处理,记录到目前为止已经处理过的最大子数组。而如果我们知道了A[1,j]的最大子数组,我们考虑A[1,j+1]的最大子数组的可能情况:
1. A[1,j+1]的最大子数组就是A[1,j]的最大子数组
2. A[1,j+1]的最大子数组为形如A[i..j+1]的最大子数组
而在我们在已知A[1..j]的最大子数组maxSum的情况下,我们可以很容易地在线性时间内找到A[1..j+1]的最大子数组。
考虑包含A[j+1]的最大子数组是否是整个数组的最大子数组,我们只需找到紧邻A[j+1]的和为最大可能正数的连续子串(∑A[i, j]max+A[j+1]就是包含A[j+1]的最大和,∑A[i, j]max必须大于等于0才有意义,加个负数只能让包含A[j+1]的和更小),我们在下面的伪代码中用sum保存这个A[i..j+1]的和。
然后sum = sum+A[j+1]就是包含A[j+1]的最大子数组的和。验证它是否是整个数组的最大子数组,只需比较sum和A[1, j]最大子数组和maxSum即可,是则更新maxSum,否则向后遍历。
伪代码:
FindMaxSubarray(A,low,high)
1. sum = 0
2. maxLeft = low tempLeft = low
3. maxRight = low
4. maxSum = A[low]
5. for i = low to high
6. if sum >= 0
7. sum = sum + A[i]
8. else sum = A[i]
9. tempLeft = i
10. if sum > maxSum
11. maxSum = sum
12. maxLeft = tempLeft
13. maxRight = i
14. return (maxLeft,maxRight,maxSum)
伪代码讲解:
第一到四行进行初始化,其中sum代表包含A[j+1] 的最大子串,这个子串可能就是A[j+1]本身(当在A[j+1]前找不到紧邻的任何和为正的连续子串时,伪代码第8行); tempLeft代表包含A[j+1]的最大子串的左边界,这个值可能篡夺已知最大子数组左边界maxLeft的位置(只要包含A[j+1]的最大子数组的和比已知最大子数组和大)
第五到十二行为循环主体,当sum <= maxSum时即为第一种情况,当sum > maxSum时,即为第二种情况
第十三行返回执行结果
C语言完整代码:
info Find_Max_Subarray(int A[], int low, int high) { int sum = 0; int tempLeft = low; info inf; inf.maxLeft = low; inf.maxRight = low; inf.Sum = 0; for (size_t i = low; i <= high; i++) { if (sum >= 0) { sum += A[i]; } else { sum = A[i]; tempLeft = i; } if (sum > inf.Sum) { inf.Sum = sum; inf.maxLeft = tempLeft; inf.maxRight = i; } } return inf; }