P-数学程序猿今天终于要拿起笔来写写画画了٩(◕‿◕。)۶,
问题来了——最大子列和
今天我们就来谈谈最大子列和问题吧,
要从一连串的数字中找到最大的连续子序列的和,
就比如数组[-2,1,-3,4,-1,2,1,-5,4],连续子数组 [4,-1,2,1] 的和最大,为 6,
可连续子序列有这么多,哪个才是最大的呀,还真是让人头疼啊 ༼ ╥ ل ╥ ༽
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
问题建模
话不多说,先为我们的问题构建测试代码吧(算法才是关键,此处可忽略,虽然这里的代码才是最耗时间和耐心的)

1 #include <iostream> 2 #include <vector> 3 #include <ctime> 4 using namespace std; 5 6 #define random(x) -x+1+rand()%(2*x-1) //用宏来定义随机数,生成(-x,x)的随机整数 7 8 void fillRandom_Int(vector<int> & nums,int cnt,int maxmum);//生成随机数组 9 int maxSubArray_test(vector<int>& nums);//引用leetcode上的官方题解,用于验证 10 bool judge(vector<int> & nums,int ans); //利用leetcode上的官方题解进行答案验证 11 int maxSubArray1(vector<int> & nums); //三重暴力循环 12 int maxSubArray2(vector<int> & nums); //两重暴力循环 13 int maxSubArray3(vector<int> & nums); //分而治之 14 int maxSubArray4(vector<int> & nums); //在线处理(贪心算法) 15 int maxSubArray5(vector<int> & nums); //动态规划 16 void test(int datasize,int (*function)(vector<int> &)); //测试并输出测试结果 17 18 int main() 19 { 20 srand((int)time(NULL)); //产生随机数种子 21 cout << endl; 22 /*cout << "\t三重暴力循环 结果 用时" << endl; 23 cout << "\t随机测试点1";test(10,maxSubArray1); 24 cout << "\t随机测试点2";test(100,maxSubArray1); 25 cout << "\t随机测试点3";test(1000,maxSubArray1); 26 cout << "\t随机测试点4 超出时间限制" << endl; 27 cout << "\t随机测试点5 超出时间限制" << endl; 28 cout << endl << endl; 29 cout << "\t两重暴力循环 结果 用时" << endl; 30 cout << "\t随机测试点1";test(10,maxSubArray2); 31 cout << "\t随机测试点2";test(100,maxSubArray2); 32 cout << "\t随机测试点3";test(1000,maxSubArray2); 33 cout << "\t随机测试点4";test(10000,maxSubArray2); 34 cout << "\t随机测试点5 超出时间限制" << endl; 35 cout << endl << endl;*/ 36 cout << "\t分而治之 结果 用时" << endl; 37 cout << "\t随机测试点1";test(10,maxSubArray3); 38 cout << "\t随机测试点2";test(100,maxSubArray3); 39 cout << "\t随机测试点3";test(1000,maxSubArray3); 40 cout << "\t随机测试点4";test(10000,maxSubArray3); 41 cout << "\t随机测试点5";test(100000,maxSubArray3); 42 cout << endl << endl; 43 cout << "\t贪心算法 结果 用时" << endl; 44 cout << "\t随机测试点1";test(10,maxSubArray4); 45 cout << "\t随机测试点2";test(100,maxSubArray4); 46 cout << "\t随机测试点3";test(1000,maxSubArray4); 47 cout << "\t随机测试点4";test(10000,maxSubArray4); 48 cout << "\t随机测试点5";test(100000,maxSubArray4); 49 cout << endl << endl; 50 cout << "\t动态规划 结果 用时" << endl; 51 cout << "\t随机测试点1";test(10,maxSubArray5); 52 cout << "\t随机测试点2";test(100,maxSubArray5); 53 cout << "\t随机测试点3";test(1000,maxSubArray5); 54 cout << "\t随机测试点4";test(10000,maxSubArray5); 55 cout << "\t随机测试点5";test(100000,maxSubArray5); 56 return 0; 57 } 58 59 void fillRandom_Int(vector<int> & nums,int cnt,int maxmum) 60 { //nums为生成的随机数组,cnt为数组元素个数,mammum为数组元素范围 61 nums.resize(cnt); 62 for(int i = 0;i < cnt;++i) 63 nums[i] = random(maxmum); 64 } 65 66 int maxSubArray_test(vector<int> & nums) 67 { 68 int n = nums.size(); 69 int currSum = nums[0],maxSum = nums[0]; 70 for(int i = 1;i < n;++i){ 71 currSum = max(nums[i],currSum+nums[i]); 72 maxSum = max(maxSum,currSum); 73 }return maxSum; 74 } 75 76 bool judge(vector<int> & nums,int ans) 77 { 78 return ans == maxSubArray_test(nums); 79 } 80 81 void test(int datasize,int (*function)(vector<int> &)) 82 { //输入测试数据规模,测试并输出测试结果 83 vector<int> nums; 84 fillRandom_Int(nums,datasize,100); //生成随机数组 85 clock_t start = clock(); 86 int ans = function(nums); //调用测试函数 87 clock_t stop = clock(); 88 if(judge(nums,ans)){ 89 cout << " 答案正确 " 90 << (double(stop-start))/CLK_TCK << 's' << endl; 91 }else cout << " 答案错误" << endl; 92 }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
解决办法
下面就来看看最大子列和的五种解决办法吧,
方法 | 时间复杂度 | 空间复杂度 |
三重循环 | O(N^3) | O(1) |
两重循环 | O(N^2) | O(1) |
分而治之 | O(NlogN) | O(logN) |
贪心算法 | O(N) | O(1) |
动态规划 | O(N) | O(N)或O(1) |
方法一——暴力三重循环
秉承着“能用暴力解决的问题就暴力解决”的原则,我们先来看看暴力的方法,最原始,最简单,最暴力的方法莫过于遍历所有连续子序列,取它们各自的和的最大值
详细如下

int maxSubArray1(vector<int> & nums) { //三重暴力循环 int n = nums.size();int sum;int ans = INT_MIN; for(int i = 0;i < n;++i){ //i对应子序列最左边下标 for(int j = i;j < n;++j){ //j对应子序列最右边下标 sum = 0; for(int k = i;k <= j;++k) //将子序列各项加起来 sum += nums[k]; if(sum > ans) ans = sum; } }return ans; }
其中i对应子序列最左边下标,j对应子序列最右边下标,用两重循环遍历所有子序列,
再用一重循环获取子序列的和,取其最大值即可
测试结果如下(PS:各个测试点数据规模为10,100,1000,10000,100000,时间限制为10s,后面不再赘述)
方法二——暴力两重循环
俗话说“暴力也是讲究美学的”,能不能把O(N^3)的复杂度降下来呢?
暴力法,前面的两重循环遍历所有子序列是避免不了了,然而,那循环计算子列和却是值得琢磨琢磨
我们留意到,在第二重循环里,当前子列和正是前一个子列和再加上当前子列的最右边元素,即nums[j],
于是,我们可以用一个临时变量sum存储子列和,就可以减少第三重循环,
这就有了第二个算法

1 int maxSubArray2(vector<int> & nums) 2 { //两重暴力循环 3 int n = nums.size();int sum;int ans = INT_MIN; 4 for(int i = 0;i < n;++i){ 5 sum = 0; 6 for(int j = i;j < n;++j){ 7 sum += nums[j]; 8 if(sum > ans) ans = sum; 9 } 10 }return ans; 11 }
我们可以看到,复杂度也随之从O(N^3)降到O(N^2)
测试结果如下(通过了测试4)
方法三——分而治之
行里话讲:“一个优秀的程序猿,当遇到O(N^2)复杂度的问题的时候,往往会想能不能把它降到O(NlogN)复杂度”,而办法往往正是分而治之
所谓“分”,就是将原问题划分成两个或多个子问题,
所谓“治”,就是将子问题及其他可能的解治理成原问题的解
对应到最大子列和,就是将原序列等分成两个子序列,
则原序列的所有子序列,
一部分包含在左边的子序列中,一部分包含在右边的子序列中,
还有一部分子序列则跨越了划分线,可记为中间子序列,
获取三部分子序列各自的最大子列和,三者最大值即为原问题的解
代码如下

1 #define max3(x,y,z) x > y ? (x > z ? x : z) : (y > z ? y : z) //三个数取最大数 2 3 int DivideAndConquer(vector<int> & nums,int left,int right){ 4 if(left == right) return nums[left]; 5 6 int mid = (left+right)/2; //将原序列等分 7 int lsum = DivideAndConquer(nums,left,mid); //递归获取左边的最大子序列和 8 int rsum = DivideAndConquer(nums,mid+1,right);//递归获取右边的最大子序列和 9 //以划分线为起点往左往右扫描整个数组获取中间序列的最大子列和 10 int lbsum = INT_MIN,rbsum = INT_MIN,tmp = 0; 11 for(int i = mid;i >= left;--i){ //往左扫描 12 tmp += nums[i]; 13 if(tmp > lbsum) lbsum = tmp; 14 }tmp = 0; 15 for(int i = mid+1;i <= right;++i){//往右扫描 16 tmp += nums[i]; 17 if(tmp > rbsum) rbsum = tmp; 18 }return max3(lsum,rsum,lbsum+rbsum);//返回三者的最大值 19 } 20 int maxSubArray3(vector<int> & nums) 21 { //分而治之 22 int n = nums.size(); 23 return DivideAndConquer(nums,0,n-1); 24 }
测试结果如下(五个测试均没超时)
方法四——贪心算法
我们可以通过子序列最右边元素将所有子序列分为N类,
从左往右扫描,每扫描一个元素,就相当于多一类子序列,
那么这类子序列的最大子列和是多少呢?
如果前一类子序列的最大子列和非负,则是前一类子序列最大子列和加上扫描的元素;
不然则是扫描的元素本身;
如此获取各类子序列的最大子列和的最大值即为所有子序列的最大值
代码如下

1 int maxSubArray4(vector<int> & nums) 2 { //贪心算法 3 int n = nums.size(); 4 int currSum = nums[0],maxSum = nums[0]; 5 for(int i = 1;i < n;++i){ 6 currSum = currSum < 0 ? nums[i] : currSum+nums[i]; 7 if(currSum > maxSum) maxSum = currSum; 8 }return maxSum; 9 }
测试结果如下(基本瞬出结果)(~ ̄▽ ̄)~
方法五——动态规划
其实动态规划换汤不换药,跟贪心算法类似,
通过子序列最右边元素将所有子序列分为N类,
从左往右扫描,每扫描一个元素,就相当于多一类子序列,
用数组记录各类子序列和,其中最大值即为所有子序列的最大值
若新建数组,则空间复杂度为O(N),若在原数组修改,则空间复杂度为O(1)
代码如下

1 int maxSubArray5(vector<int> & nums) 2 { //动态规划 3 int n = nums.size(); 4 vector<int> sums = nums; 5 int maxSum = nums[0]; 6 for(int i = 1;i < n;++i){ 7 if(sums[i-1] >= 0) sums[i] += sums[i-1]; 8 if(sums[i] > maxSum) maxSum = sums[i]; 9 }return maxSum; 10 }
测试结果(同样瞬出)(~ ̄▽ ̄)~
回顾5种算法,暴力,暴力美学,分而治之,贪心算法,动态规划,将问题的复杂度从O(N^3)一直降到O(N),这就是算法的威力,也是我为什么选择它作为我的第一篇正式的博客,希望能对大家有所帮助和启迪
这次写博也是有不少缺陷的,比如动态规划和贪心算法方面没有画图详细说明(希望以后能找到一个好的画图软件,电脑手写数字实在太丑了(ó﹏ò。))
最后,安利浙江大学陈越老师的慕课《数据结构》(★>U<★),本文很多都是从中获得启发,
晚安(V●ᴥ●V),各位~~~~