和最大的连续子数组
求一个数组中,和最大的子数组,是一个比较经典的题目,《算法导论》中有一个用分治的经典解法,时间复杂度可以做到O(nlgn).
《编程之美》,《编程玑珠》中都有讨论这个问题,其中一个动态规划的做法非常精巧,思路很赞,时间复杂度也优化到了O(n).
上面提到的两种解法对分治法,动态规划很有启发性,是非常好的练习题目。
(1)分治法:
关键的思想在于把大题目转化为对多个小题目的求解。
考虑,如果我们把数组arr[],分成大小相等的两半:arr_a与arr_b,那么当前数组的最大子和sub, 只有三种可能:
1)sub 完全在 arr_a中。
2)sub 完全在arr_b中。
3)sub 横跨arr_a与arr_b.
对于1), 2)这显然是当前问题的子问题,可以递归的求解。
需要关注的是3).
对于3),可以转换为这样的问题:
假设中间点为mid,求一个包含arr[mid]的和最大的子数组。
这个问题又等价于:求一个以arr[mid]为是后一个元素的和最大的子数组,及求一个以arr[mid]为开头的和最大的子数组。
这两个问题显然可以直接通过累加来暴力解出的。
struct tuple { int s; int e; int sum; }; tuple MaxSub(int arr[], int low,int high) { if(low < high) { int mid = (low + high)/2; tuple left = MaxSub(arr,low,mid); tuple right = MaxSub(arr,mid+1,high); tuple middle = MaxMiddle(arr,low,high); return max(left,right,middle); } return {low,low,arr[low]}; }
至于MaxMiddle,我们直接暴力计算就可以了。
tuple MaxMiddle(int arr,int low,int high) { int mid = (low + high)/2; int max_left = 0, li = mid, tmp_sum = 0; for(int i = mid; i >= 0; i--) { tmp_sum += arr[i]; if(tmp_sum > max_left) { max_left = tmp_sum; li = i; } } int max_right = 0, ri = mid; tmp_sum = 0; for(int i = mid; i < high, ++i) { tmp_sum += arr[i]; if(tmp_sum > max_right) { max_right = tmp_sum; ri = i; } } return {li,ri, max_left+max_right - arr[mid]}; }
这里用分治法的好处是思路非常清晰,代码也比较容易写,且时间复杂度也控制的很好。
但这个题目还有更好的更优的解法,如:动态规划。
(2) 动态规划:
这个解法的思路与分治法有些神的相似。
如下考虑:
对于数组arr[0]...arr[n].
假设我们已经求出了子数组:arr[1] ... arr[n]的最大的子数组 arr[i] .... arr[j].及以arr[1]的开头的最大的子数组:arr[1]....arr[e].
那么arr[0]...arr[n]的最大的子数组必然为以下三种情况之一:
1)arr[0],
2)arr[0] + arr[1] + ... + arr[e].
3)arr[i]+...+arr[j].
所以问题就分解为怎样求子数组arr[1]....arr[n]的最大子数组和,及最大以arr[1]开关的子数组和。
对于arr[n],显然,它的最大子数组为arr[n],最大以arr[n]开头的子数组,也是arr[n].
假设我们已经求得到了当arr[k]....arr[n]的最大子数组为:arr[ki]....arr[kj].
最大以arr[k]开头的子数组为:arr[k]....arr[ke].
则对于arr[k-1]....arr[n]. 它的最大的以arr[k-1]开头的子数组只有两种情况:
1)arr[k-1].
2)arr[k - 1] + arr[k] +...+ arr[ke].
而arr[-1]...arr[n]的最大子数组就只有两种情况:
1)arr[k-1]开头的最大子数组,就是上面所求的结果。
2) arr[k]...arr[n]的最大子数组。
void sovle(int arr[], int sz) { int max[sz]; int max2[sz]; max[sz - 1] = arr[sz-1]; max2[sz-1] = arr[sz-1]; for(int i = sz - 2; i >= 0; --i) { if(max2[i+1) >= 0) max2[i] = arr[i]+max2[i+1]; else max2[i] = arr[i]; if(max2[i] > max[i+1]) max[i] = max[2]; else max[i] = max[i+1]; } cout << max[0] << endl; }
动态规划的做法非常巧妙,而且时间复杂度也优化到了O(n).
上面的伪代码中,空间复杂度为O(n),事实上还可以优化为到O(1),这里就不介绍了。
(3)
对于这个题目,我还在学校的时候第一次碰到,当时并没有像上面那些书中提到的一样想得深入和复杂,但也找到一个比较山寨的解法的,时间复杂度能做到O(n).
题目事实上并没有很难,我们只要有一个变量记录一下当前累加的和,以及一个到目前为止,发现的最大的和,扫完一遍数组,就可以得到答案了。
1)假设arr[0]为要找的子数组的第一个元素,sum = arr[0], temp_sum = arr[0];
s = e = ts = te = 0; (注:s == start,e == end, ts == temp start, te == temp end)
2) temp_sum += arr[i]; i = 1,2,3,....
3)
a)如果 temp_sum > sum, s = ts,e=te.
b)如果 temp_sum < 0, ts = te = i + 1; temp_sum = 0;
这里把ts 挪到i 之后是正确的,因为这里temp_sum < 0,说明之前的数组加起来都是负的,已经没有必要再去重新累加了。
#include <iostream> using namespace std; void solve(int arr[] ,int sz) { int max, mi,mj; int i,m; max = arr[0]; mi=mj=0; m = i = 0; for(int s = 0;s < sz ; ++s) { m+=arr[s]; if(m > max) { max = m; mi = i; mj = s; } if(m < 0) { i = s + 1; m = 0; } } if( mi < sz) cout << "max sub-array: from " << mi << " to " << mj << ", sum = " << max << endl; } int main() { int *arrs[] = { {1,2,3,4,5}, {-2,2,3,-4,3}, { -100, 100, -1, 100, -100} }; return; }