最大子数组和
- 问题描述
- 枚举法
- 分治法
- 动态规划
- 数据测试
问题描述
给定一个有n(n>0)个整数的序列,要求其连续子数组,使得这子数组对应的元素和最大。
输入:整数序列{nums}。
输出:最大子数组和。
我们还可以求,m个连续不相交的子数组最大和,即最大m子段和
枚举法
枚举法是最容易想到的:
我们只需求的arr[i]+arr[i+1]+…+arr[j],0<=i<=j<=n的所有结果,并求出其中最大值。
- 伪代码:
FUNCTION MaxSubArrayEx(nums) :// Input:整数数组 nums// Output:连续子数组的最大和
Beginn ← LENGTH(nums)Max ← nums[0] FOR i FROM 0 TO n - 1 :temp ← 0FOR j FROM i TO n - 1 :temp ← temp + nums[j]IF temp>MaxMax ← tempEnd IFEnd ForEnd ForRETURN maxSum
End MaxSubArrayEx
- 代码实现:
int MaxSubArrayEx(vector<int>& nums)
{int Max = nums[0];for (int i = 0; i < nums.size(); i++){int temp = 0;for (int j = i; j < nums.size(); j++){temp += nums[j];Max = max(temp, Max);}}return Max;
}
- 复杂度分析:
显然我们需要求1+2+…+n=n(n+1)/2=O(n2).即时间复杂度为O(n2),空间复杂度为O(1).
分治法
首先很容易想到把数组从下标从[1,n]拆开为[1,m],[m+1,n]两个子数组,其中m=n/2。
然后分别求出两个子数组的最大子数组和,再合并答案。
但是合并过程中,我们发现原数组的最大子数组和对应的数组其下标有可能为[l,r],其中l<=m,r>=m+1.也就是说这个数组横穿了两个子数组,其中在左子数组的部分为[l,m]则这个子数组应当是左子数组以num[m-1]结尾的最大子数组和对应的子数组。
事实上,如果这个子数组并非左子数组以num[m-1]结尾的最大子数组和对应的子数组,那么存在子数组[l`,m]是以num[m-1]结尾的最大子数组和对应的子数组,也就是说[l`,m]对应数组元素的和大于[l,m]对应数组元素的和。那么[l`,r]对应数组元素的和大于[l,r]对应数组元素的和,该结论与[l,r]为原数组的最大子数组和对应的数组矛盾。
因此[l,m] 是左子数组以num[m-1]结尾的最大子数组和对应的子数组。
同理,[m+1,r]是右子数组以nums[m]为起点的最大子数组和对应的子数组。
也就是说对于[l,r]这个子数组,我们需要目前维护三个变量lsum以l为起点的最大子数组和,rsum以r为起点的最大子数组和,以及msum即区间内的最大子数组和。
那么如何更新lsum和rsum呢?
看回原数组[1,n]其lsum对应的子数组为[1,x],x有两种情况:x<=m,那很显然[1,x]应当为左子数组的以nums[1]为起点的最大子数组和对应的子数组,证明同上;x>m,那就是[1,m]+[m+1,x].显然[m,x]应当为右子数组的以nums[m]为起点的最大子数组和对应的子数组,证明同上。
rsum的维护同理。那么我们就还需要维护一个变量isum即[l,r]的区间和。
注意到,l=r时,lsum=rsum=msum=isum=nums[l-1].
- 伪代码:
FUNCTION Get(nums, l, r) :
Begin// 输入:数组 nums,区间 [l, r]// 输出:包含四个值的数组 [isum, lsum, msum, rsum]DECLARE sum[4] // sum[0] = isum, sum[1] = lsum, sum[2] = msum, sum[3] = rsumIF l == r:sum[0] ← nums[l]sum[1] ← nums[l]sum[2] ← nums[l]sum[3] ← nums[l]ELSE :m ← l + ((r - l) / 2)L ← Get(nums, l, m)R ← Get(nums, m + 1, r)sum[0] ← L[0] + R[0] sum[1] ← MAX(L[1], L[0] + R[1]) sum[2] ← MAX(L[2], R[2], L[3] + R[1]) sum[3] ← MAX(R[3], R[0] + L[3]) RETURN sumEnd IF
End GetFUNCTION MaxSubArrayDc(nums) :
Begin// 输入:整数数组 nums// 输出:最大子数组和(分治法)result ← Get(nums, 0, LENGTH(nums) - 1)RETURN result[2]
End MaxSubArrayDc
- 代码实现:
vector<int> Get(vector<int>& nums, int l, int r)
{vector<int>sum(4);//sum[0]为isum,sum[1]为lsum,sum[2]为msum,sum[3]为rsumif (l == r){for (int i = 0; i < 4; i++)sum[i] = nums[l ];}else{int m = l + (r - l) / 2;vector<int>L = Get(nums, l, m);vector<int>R = Get(nums, m + 1, r);sum[0] = L[0] + R[0];sum[1] = max(L[1], L[0] + R[1]);sum[2] = max({ L[2],R[2],L[3] + R[1] });sum[3] = max(R[3], R[0] + L[3]);}return sum;
}
int MaxSubArrayDc(vector<int>& nums)
{return Get(nums, 0, nums.size()-1)[2];
}
- 复杂度分析:
时间复杂度:将赋值操作看作基本操作,对于样本量n的基本操作次数f(n)有f(n)=2f(n/2)+4,f(1)=4.令n=2k,g(k)=f(n),则g(k)=2f(n/2)+4=2g(k-1)+4,将其展开可以得到g(k)=2kg(0)+4(1+2+4+…+2k-1)=8*2k-4=8n-4=f(n)=O(n).即时间复杂度为O(n).
空间复杂度:对于每一个函数Get会生成常数个变量,我们总共调用了2logn-1次Get,即n-1次.故空间复杂度为O(n).
动态规划
既然提到了动态规划法,我们首先就要想到能否通过这个问题的子问题来推导该问题的答案。也就是说能不能先求出n-1个整数的整数序列的最大子数组和,再推导出n个整数的整数序列的最大子数组和。
很显然这是行不通的,原因如下:
1)首先,n个整数的整数序列里面选出n-1个整数,有n种结果。
2)其次,这n种结果里面有n-2种子数组是不连续的,那么其最大子数组和就失去了意义,因为无法合并为n个整数的整数序列的最大子数组和。
但通过错误的尝试,我们发现了第一个关键点,就是子结构也就是n-1个整数的子数组应当在原数组中连续。那么我们考虑前n-1个整数的子数组也就是nums1,nums2,…,numsn-1.能否求得这个子数组的最大子数组和然后再和numsn合并为n个整数的序列的最大子数组和?
通过简单的分析后我们发现,这也是行不通的。因为我们不确保前n-1个整数序列的最大子数组和所对应的子数组是以numsn-1结尾的,那就不能简单的加上numsn来探讨。
那也就是说我们必须求出以numsn-1结尾的最大子数组和。
经过上述分析,我们终于得到了一种可行的状态表示。
记dp[i]为以numsi结尾的最大子数组和。事实上这也是我们动态规划常用的经验状态表示。
那么对于以numsi+1结尾的最大子数组和对应的子数组有两种情况,要么就是只有他本身,要么就是numsi+1加上以numsi结尾的最大子数组和对应的子数组。
也就是说,状态转移方程为dp[i+1]=max{numsi+1,numsi+1+dp[i]}.初始化dp[1]=nums1。
那么我们所求的整数序列的最大子数组和就是max{dp}。注:下面代码中nums[i]=numsi+1.
- 伪代码:
FUNCTION MaxSubArrayDp(nums) :// 输入:整数数组 nums// 输出:连续子数组的最大和
Beginn ← LENGTH(nums)DECLARE dp[1:n] dp[1] ← nums[0] maxSum ← dp[1] FOR i FROM 2 TO n :currentElement ← nums[i - 1] dp[i] ← MAX(currentElement, currentElement + dp[i - 1])maxSum ← MAX(maxSum, dp[i])End ForRETURN maxSum
End MaxSubArrayDp
- 代码实现:
int MaxSubArrayDp(vector<int>& nums)
{vector<int>dp(nums.size() + 1);dp[1] = nums[0];int Max = dp[1];for (int i = 2; i <= nums.size(); i++){dp[i] = max(nums[i - 1], nums[i - 1] + dp[i - 1]);Max = max(dp[i], Max);}return Max;
}
- 复杂度分析:
不难发现时间复杂度O(n),空间复杂度O(n).
数据测试
下面我们对数据量20,1000,10000的数据进行测试:
#include<iostream>
#include<vector>
#include<algorithm>
#include<climits>
#include<fstream>
#include<chrono>
#include<cstdlib>
using namespace std;
using namespace chrono;
vector<int> GenerateArray(int n, int minVal = -1000, int maxVal = 1000) {vector<int> nums(n);for (int i = 0; i < n; i++) {nums[i] = rand() % (maxVal - minVal + 1) + minVal;}return nums;
}// 测试函数
void RunTest(int N, ofstream& csv_out) {cout << "\n===== N = " << N << " =====" << endl;vector<int> nums = GenerateArray(N);double timeEx = -1, timeDp = -1, timeDc = -1;if (N <= 1000) {auto start = high_resolution_clock::now();int resEx = MaxSubArrayEx(nums);auto end = high_resolution_clock::now();timeEx = duration<double, milli>(end - start).count();cout << "Exhaustive: Result = " << resEx << ", Time = " << timeEx << " ms" << endl;}auto start = high_resolution_clock::now();int resDp = MaxSubArrayDp(nums);auto end = high_resolution_clock::now();timeDp = duration<double, milli>(end - start).count();cout << "Dynamic Programming: Result = " << resDp << ", Time = " << timeDp << " ms" << endl;start = high_resolution_clock::now();int resDc = MaxSubArrayDc(nums);end = high_resolution_clock::now();timeDc = duration<double, milli>(end - start).count();cout << "Divide & Conquer: Result = " << resDc << ", Time = " << timeDc << " ms" << endl;// 校验一致性if (N <= 1000) {if (resDp == resDc && resDc == MaxSubArrayEx(nums))cout << "Result Check: PASSED" << endl;elsecout << "Result Check: FAILED" << endl;}else {if (resDp == resDc)cout << "Result Check (DP vs DC): PASSED" << endl;elsecout << "Result Check (DP vs DC): FAILED" << endl;}
}int main() {srand(time(0));ofstream csv_out("time_results.csv");csv_out << "N,Exhaustive,DP,DivideConquer,MultiSegment\n";RunTest(20, csv_out);RunTest(1000, csv_out);RunTest(10000, csv_out);csv_out.close();return 0;
}
运行结果:
我们发现分治法和动态规划法的理论时间复杂度都是O(n),但我们的分治法明显比动态规划法慢得多,原因如下:
- 递归开销大(函数调用&栈帧)
每个递归调用都要创建局部变量、保存上下文、返回值等;
对于大型数据,如 N=10000,将递归调用近 2N次
而动态规划只是一个简单的 for 循环,运行效率很高。 - 缺少尾递归优化
C++ 没有强制优化尾递归;
递归函数返回值是vector,需要不断地创建、拷贝,非常耗时。 - 合并操作创建了 vector(频繁分配内存)
每一层都要创建一个新的 vector;
会频繁触发内存分配和释放,比单纯使用整型变量慢很多。