求解一个数组的最大子数组,例如给定数组B[0,,,,,,n-1],就是找到满足条件的一组i和j使得当i <= j时,有B[j] - B[i]的值达到最大。这里我们很容易想到一种暴力求解的方法。从n个下标中任意选取两个,就是n*(n-1)/2中组合,来计算得到最大子数组即可。此时这种算法的时间复杂度为O(n^2),但是若我们采用如下分治策略来进行优化的话,我们可以得到一个算法时间复杂度为O(nlogn)的算法。
分治策略:首先我们来对问题进行一下转换,把求B的最大子数组的问题转换成求A的最大子数组问题,数组A即反应B中数的变化的一个数组,其中一个元素A[i](i >=1)代表的是B[i]-B[i-1],这样的话,我们看问题的角度就发生了一定的变化,即不在关心B中的每个数是多少,而是关心B中每个数相对于前一个数是如何变化的,即问题现在转化成了寻找A的和最大的非空连续子数组。
在明确了问题转化成为求解A的和最大的非空连续子数组后,我们来思考如何用分治技术来求解最大子数组问题。假定我们寻找子数组A[low.......high]的最大子数组。使用分治技术意味着我们要将子数组划分为两个规模尽量相等的子数组。也就是说,找到子数组的中央位置,比如mid,然后考虑求解两个子数组A[low....mid]和A[mid+1...high]。经分析可知,A的任何连续子数组A[i....j]所处的位置必然是以下三种情况之一:
- 完全位于子数组A[low....mid]中,因此low <= i <= j <= mid
- 完全位于子数组A[mid+1....high]中,因此mid+1 <= i <= j <= high
- 跨越了中点,因此low <= i <= j <= high
因此A[low...high]的一个最大子数组所处的位置必然是三种情况之一。实际上A[low....high]的一个最大子数组必然是完全位于A[low...mid]中,完全位于A[mid+1,high]中,或者跨越终点的所有子数组的最大者。我们可以递归地求解A[low...mid]和A[mid+1...high]的最大子数组,因为这两个子数组问题仍然是最大子数组问题,只是规模更小。因此剩下的工作就是寻找跨越重点的最大子数组,然后在三者情况中选取和最大者。
可以很容易地想到在线性时间内求出跨越中点的最大子数组。此问题并非原问题规模更小的实例,因为它加入了限制——求出的子数组必须横跨中点,任何跨越中点的子数组都由两个子数组A[i...mid]和A[mid+1...j]组成,其中low <= i <= mid 且 mid+1 <= j <= high。因此我们只需找出形如A[i...mid]和A[mid+1....j
]的最大子数组,然后将其合并即可。
在有了以上方法后,我们很容易来列出求解这个问题的最终算法:
- 若low == high 返回low,high,A[low]或者A[high]
- 计算mid = (low+high)/2
- 计算A(low,mid)的最大子数组
- 计算A(mid+1,high)的最大子数组
- 计算横跨中点的A(low,mid,high)的最大子数组
- 选取一个最大的即可
#include<iostream>
#include<climits>
using namespace std;
//定义子数组结构体,分别代表子数组的下界和上界,以及和
struct sub_array {
int max_left;
int max_right;
int max_sum;
};
//寻找跨越中点的最大子数组的函数
sub_array Find_Max_Crossing_Subarray(int *A, int low, int mid, int high) {
int left_sum = INT_MIN;
int right_sum = INT_MIN;
int sum = 0;
sub_array cross_array;
//计算中点左侧的最大子数组
for (int i = mid; i >= 1; i--) {
sum = sum + A[i];
if (sum > left_sum) {
left_sum = sum;
cross_array.max_left = i;
}
}
sum = 0;
//计算中点右侧的最大子数组
for (int j = mid + 1; j <= high; j++) {
sum = sum + A[j];
if (sum > right_sum) {
right_sum = sum;
cross_array.max_right = j;
}
}
//将二者进行合并
cross_array.max_sum = left_sum + right_sum;
return cross_array;
}
//寻找最大子数组的函数
sub_array Find_Maximun_Subarray(int *A, int low, int high) {
int mid;
if (high == low) { //数组中只有一个元素的情况
sub_array temp;
temp.max_left = low;
temp.max_right = high;
temp.max_sum = A[low];
return temp;
}
else {
mid = (low + high) / 2;
sub_array cross_array;
sub_array left_array;
sub_array right_array;
left_array = Find_Maximun_Subarray(A, low, mid); //计算左子数组
right_array = Find_Maximun_Subarray(A, mid + 1, high); //计算右子数组
cross_array = Find_Max_Crossing_Subarray(A, low, mid, high); //计算横跨中点的子数组
//寻找最大的情况
if (left_array.max_sum >= right_array.max_sum && left_array.max_sum >= cross_array.max_sum) {
return left_array;
}
else if (right_array.max_sum >= left_array.max_sum && right_array.max_sum >= cross_array.max_sum) {
return right_array;
}
else {
return cross_array;
}
}
}
int main() {
int num;
cin >> num;
int *B = new int[num];
int *A = new int[num];
for (int i = 0; i < num; i++) {
cin >> B[i];
}
for (int i = 1; i < num; i++) {
A[i] = B[i] - B[i-1];
}
sub_array solution;
solution = Find_Maximun_Subarray(A, 1, num-1);
cout << solution.max_left<<" "<< solution.max_right<<" "<<solution.max_sum << endl;
system("pause");
}
算法分析:
可以看出当n = 1时,算法的时间复杂度为O(1),当n >= 2是,T(n) = 2T(n/2) + O(n),故综上所述,算法的时间复杂度为O(nlogn),可以看出比起暴力求解的O(n^2)的时间复杂度,已经明显优化了不少。