问题:寻找数组A[ l..r ] 中各元素之和最大的连续非空子数组。我们称这样的数组叫最大子数组。
本文将给出三种解法:暴力求解、分治、动态规划。时间复杂度依次递减。
为了方便函数传出结果,我们定义一个结构体来储存最大子数组的要素,即该子数组在原数组的下标界限和对应的和。
typedef struct subArray {
int l; //子数组的左边界
int r; //子数组的右边界
int sum; //子数组每一项的和
} SUB_ARRAY;
方法一:暴力求解法 ——
显然,需要两个for循环来遍历原数组所有的子数组,时间复杂度相当高。
这个思路太差,就不上代码了。
方法二:分治法 ——
1、分(Divide)
我们要寻找一个数组 a[l, r] 的最大子数组,我们可以将该数组分解成两个规模尽量相等的子数组。即找到数组的中央位置mid,然后将其划分为左子数组 a[l, mid] 和 右子数组a[mid + 1, r] 来考虑。
2、治(Conquer)
那么a[l, r] 的子数组有三种存在方式,在左子数组内、在右子数组内、跨越中点的数组。那么应该在这三个范围里找最大子数组,前面两者我们可以通过递归地调用求解,因为这两个子问题仍然是一般情况下求最大子数组问题,只是规模更小。那么寻找跨越中点的最大子数组,我们可以单独写一个函数,在O(n)时间内,从mid向两侧遍历即可。
完整代码如下:
// Created by A on 2020/3/5.
#include <climits>
typedef struct subArray {
int l; //子数组的左边界
int r; //子数组的右边界
int sum; //子数组每一项的和
} SUB_ARRAY;
/* 在数组a[l,r]内找到包含m下标位置的最大子数组 */
SUB_ARRAY FindMaxCrossingSubarray(int a[], int l, int m, int r) {
/* 计算从m出发的左半边数组的最大子数组 */
int leftMax = INT_MIN, leftIndex, t = 0;
for (int i = m; i >= l; i--) {
t += a[i];
if (t > leftMax) {
leftIndex = i; //最大位置对应的下标
leftMax = t; //最大和
}
}
t = 0;
/* 计算从m + 1出发的右半边数组的最大子数组 */
int rightMax = INT_MIN, rightIndex;
for (int i = m + 1; i <= r; i++) {
t += a[i];
if (t > rightMax) {
rightIndex = i; //最大位置对应的下标
rightMax = t; //最大和
}
}
SUB_ARRAY ans;
ans.l = leftIndex;
ans.r = rightIndex;
ans.sum = leftMax + rightMax;
return ans;
}
/* 在数组a[l,r]内找到最大子数组(非空!) */
SUB_ARRAY FindMaxSubarray(int a[], int l, int r) {
/* 递归的终点,数组只有一个元素 */
if(l == r) {
SUB_ARRAY ans;
ans.l = ans.r = l;
ans.sum = a[l];
return ans; //直接将原数组作为最大子数组返回
}
int mid = (l + r) / 2; //中间位置
SUB_ARRAY leftAns = FindMaxSubarray(a, l, mid); //递归地求mid左侧的最大子数组
SUB_ARRAY rightAns = FindMaxSubarray(a, mid + 1, r); //递归地求mid右侧地最大子数组
SUB_ARRAY midAns = FindMaxCrossingSubarray(a, l, mid, r); //求包含mid地最大子数组
/* 返回三者中和最大的 */
if(leftAns.sum > midAns.sum && leftAns.sum > rightAns.sum)
return leftAns;
else if(midAns.sum > rightAns.sum)
return midAns;
else
return rightAns;
}
方法三:动态规划法 ——
1、算法描述
这个是一个很棒的非递归、线性时间复杂度的方法:
若已知数组 a[0..j] 的最大子数组,那么 a[0..j+1] 的最大子数组为下面两种情况之一:
- 不包含第j + 1项:即为数组 a[0..j] 的最大子数组
- 包含第j+1项:a[k..j+1] (其中 0<= k <= j + 1)
2、算法实现
从头开始循环遍历数组a,遍历到的下标索引为 i :用cur记录 a[ 0..i ] 内包含第 i 项的最大子数组,用ans记录 a[ 0..i ] 内的最大子数组。(下面解释时用cur()、ans()分别表示两个变量,括号内为遍历到下标索引,来代表在遍历到这个位置所对应的cur与ans。)
若已知 cur(i) 和 ans(i),那么继续遍历下一个元素:
- 根据cur 的定义,cur(i+1) 应该是 cur(i) + a[ i + 1] 和 a[ i + 1] 中的较大值
- 根据算法描述中说的:ans(i+1)应该是cur(i+1) 和 ans(i)中的较大值。那么遍历完毕的 ans 就是答案。
下面是一张遍历的具体算法图:
算法理解了,代码就很简单了:
#include <climits>
typedef struct subArray {
int l; //子数组的左边界
int r; //子数组的右边界
int sum; //子数组每一项的和
} SUB_ARRAY;
/* 在数组a[l,r]内找到最大子数组(非空!) */
SUB_ARRAY FindMaxSubarray1(int a[], int l, int r) {
int ans = INT_MIN, cur = INT_MIN;
int curLeft, leftIndex, rightIndex; //记录cur对应的子数组左下标、记录ans对应的子数组左右下标
for (int i = l; i <= r; i++) {
/* 找到a[ 0..i ]内包含第i项的最大子数组 */
if (cur + a[i] >= a[i])
cur += a[i];
else {
cur = a[i];
curLeft = i;
}
/* 更新a[ 0..i ]内的最大子数组 */
if (cur > ans) {
ans = cur;
leftIndex = curLeft;
rightIndex = i;
}
}
/* 返回答案 */
SUB_ARRAY result;
result.l = leftIndex;
result.r = rightIndex;
result.sum = ans;
return result;
}
end
欢迎关注个人公众号“ 鸡翅编程 ”,这里是认真且乖巧的码农一枚。
---- 做最乖巧的博客er,做最扎实的程序员 ----
旨在用心写好每一篇文章,平常会把笔记汇总成推送更新~