问题描述
给定由n个整数(包含负整数)组成的序列a1,a2,…,an,求该序列子段和的最大值。规定当所有整数均为负值时定义其最大子段和为0
穷举法
最简单的方法就是穷举法,用一个变量指示求和的开始位置,一个变量指示结束位置,再一个变量指示当前要加和的位置,每一个开始位置对应n-i个结束位置,遍历一遍就能得到最大值
int maxSubArray(int a[], int n) {int maxsum = 0;for (int i = 0; i < n; i++) {//开始位置for (int j = i; j < n; j++) {//结束位置int nowsum = 0;for (int k = i; k <= j; k++) {nowsum += a[k];if (nowsum > maxsum)maxsum = nowsum;}}}return maxsum;
}
算法有三重循环,时间复杂性为O(n^3)
穷举法优化
当字段的开始下标确定后,要计算[i:j]的字段和可以利用上一次计算的[i:j-1]的字段和,加上a[j]就可以了
int maxSubArray2(int a[], int n) {int maxsum = 0;for (int i = 0; i < n; i++) {//开始位置int nowsum = 0;for (int j = i; j < n; j++) {//结束位置nowsum += a[j];if (nowsum > maxsum)maxsum = nowsum;}}return maxsum;
}
改进后的时间复杂度为O(n^2)
分治法
该问题也可以用分治法解决
分治策略思想如下:
将所给序列a[1:n]
分成长度相同的两端a[1:n/2]
、a[n/2 +1:n]
,分别求出这两段的最大子段和,则整体序列a[1:n]的最大子段和有以下三种情况
- a[1:n]的最大子段和与a[1:n/2]的最大子段和相同
- a[1:n]的最大子段和与a[n/2 +1:n]的最大子段和相同
- a[1:n]的最大子段和是a[1:n/2]最后一段加a[n/2 +1:n]最开始一段的和
前两种情况可以递归求得。对于第三种情况,可以发现,a[n/2]和a[n/2 +1]都在最大子段里,我们可以从a[n/2]向左、从a[n/2 +1]向右分别计算两个最大字段和s1和s2,s1+s2就是最大子段和
递归方程
MaxSum ( l o w , h i g h ) = { max ( 0 , arr [ l o w ] ) if l o w = h i g h max { MaxSum ( l o w , m i d ) MaxSum ( m i d + 1 , h i g h ) CrossSum ( l o w , m i d , h i g h ) otherwise \text{MaxSum}(low, high) = \begin{cases} \displaystyle \max\left(0,\, \text{arr}[low]\right) & \text{if } low = high \\ \displaystyle \max \begin{cases} \text{MaxSum}(low, mid) \\ \text{MaxSum}(mid+1, high) \\ \text{CrossSum}(low, mid, high) \end{cases} & \text{otherwise} \end{cases} MaxSum(low,high)=⎩ ⎨ ⎧max(0,arr[low])max⎩ ⎨ ⎧MaxSum(low,mid)MaxSum(mid+1,high)CrossSum(low,mid,high)if low=highotherwise
代码
int maxSubArray3(int a[], int left, int right) {if (left == right)return a[left];int mid = (left + right) / 2;int maxleft = maxSubArray3(a, left, mid);int maxright = maxSubArray3(a, mid + 1, right);int maxleftsum = 0, maxrightsum = 0;int nowsum = 0;for (int i = mid; i >= left; i--) {nowsum += a[i];if (nowsum > maxleftsum)maxleftsum = nowsum;}nowsum = 0;for (int i = mid + 1; i <= right; i++) {nowsum += a[i];if (nowsum > maxrightsum)maxrightsum = nowsum;}return max(maxleft, max(maxright, maxleftsum + maxrightsum));
}
T ( n ) = { 2 T ( n 2 ) + O ( n ) n > c O ( 1 ) n ≤ c T(n)=\big \{^{O(1) \quad n\le c}_{2T(\frac{n}{2})+O(n) \quad n>c} T(n)={2T(2n)+O(n)n>cO(1)n≤c
根据master定理,我们得到
T ( n ) = O ( n l o g ) T(n)=O(nlog) T(n)=O(nlog)
比起穷举法的O(n^2)更优了
动态规划
设数组为 a[1..n]
,定义状态 b[i]
表示以 a[i]
结尾的子段的最大和,则最大字段和为 m a x b j max b_j maxbj
有递归方程:
b [ i ] = { 0 i = 0 ( 边界条件 ) max ( b [ i − 1 ] + a [ i ] , a [ i ] ) i ≥ 1 b[i] = \begin{cases} 0 & i = 0 \quad (\text{边界条件}) \\ \max(b[i-1] + a[i],\, a[i]) & i \ge 1 \end{cases} b[i]={0max(b[i−1]+a[i],a[i])i=0(边界条件)i≥1
全局最大子段和为所有 b[i]
中的最大值,并与0比较:
MaxSum = max ( 0 , max 1 ≤ i ≤ n b [ i ] ) \text{MaxSum} = \max\left(0,\, \max_{1 \le i \le n} b[i]\right) MaxSum=max(0,1≤i≤nmaxb[i])
计算最优值
使用变量b记录此前最大字段和,如果为负数则当前最大和为a[i]
,如果为正数则最大和为b+a[i]
代码
int maxSubArray4(int a[], int n) {int b = 0;int maxsum = 0;for (int i = 0; i < n; i++) {if (b > 0)b += a[i];else b = a[i];if (b > maxsum)maxsum = b;}return maxsum;
}
构造最优解
使用两个变量start
和end
记录最大字段的起始和结束位置
int maxSubArray4(int a[], int n, int* start, int* end) {int b = 0;int max_sum = 0;int current_start = 0; // 当前子段起始位置*start = *end = -1; // 默认无效索引for (int i = 0; i < n; i++) {if (b > 0) {b += a[i];}else {b = a[i];current_start = i; // 重置起点}// 更新全局最大值及索引if (b > max_sum) {max_sum = b;*start = current_start;*end = i;}}// 处理全负数情况:返回0且不记录索引if (max_sum <= 0) {*start = *end = -1;return 0;}return max_sum;
}
时间负责度:O(n)
实例
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//穷举法
int maxSubArray(int a[], int n) {int maxsum = 0;for (int i = 0; i < n; i++) {//开始位置for (int j = i; j < n; j++) {//结束位置int nowsum = 0;for (int k = i; k <= j; k++) {nowsum += a[k];if (nowsum > maxsum)maxsum = nowsum;}}}return maxsum;
}
//穷举法优化
int maxSubArray2(int a[], int n) {int maxsum = 0;for (int i = 0; i < n; i++) {//开始位置int nowsum = 0;for (int j = i; j < n; j++) {//结束位置nowsum += a[j];if (nowsum > maxsum)maxsum = nowsum;}}return maxsum;
}
//分治法
int maxSubArray3(int a[], int left, int right) {if (left == right)return a[left];int mid = (left + right) / 2;int maxleft = maxSubArray3(a, left, mid);int maxright = maxSubArray3(a, mid + 1, right);int maxleftsum = 0, maxrightsum = 0;int nowsum = 0;for (int i = mid; i >= left; i--) {nowsum += a[i];if (nowsum > maxleftsum)maxleftsum = nowsum;}nowsum = 0;for (int i = mid + 1; i <= right; i++) {nowsum += a[i];if (nowsum > maxrightsum) {maxrightsum = nowsum;}}return max(maxleft, max(maxright, maxleftsum + maxrightsum));
}
//动态规划
int maxSubArray4(int a[], int n, int* start, int* end) {int b = 0;int max_sum = 0;int current_start = 0; // 当前子段起始位置*start = *end = -1; // 默认无效索引for (int i = 0; i < n; i++) {if (b > 0) {b += a[i];}else {b = a[i];current_start = i; // 重置起点}// 更新全局最大值及索引if (b > max_sum) {max_sum = b;*start = current_start;*end = i;}}// 处理全负数情况:返回0且不记录索引if (max_sum <= 0) {*start = *end = -1;return 0;}return max_sum;
}
int main() {int a[] = { 1, -2, 3, 10, -4, 7, 2, -5 };cout << maxSubArray(a, 8) << endl;cout << maxSubArray2(a, 8) << endl;cout << maxSubArray3(a, 0, 7) << endl;int start, end;cout << maxSubArray4(a, 8, &start, &end) << endl;cout <<"start:" << start << " " <<"end:"<< end << endl;return 0;
}
运行结果