最大子段和详解(N种解法汇总)

本文详细解析了求解最大子段和问题的各种算法,包括穷举法、分治策略、动态规划及一种特殊解法,对比不同方法的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

必背模板

 18 int fun(int a[],int n)
 19 {
 20     int i,sum=0,MAXSum=0;
 21     for(i=0;i<n;i++)
 22     {
 23         if(sum>0)
 24             sum+=a[i];
 25         else
 26             sum=a[i];
 27         if(sum>MAXSum)
 28             MAXSum=sum;
 29     }
 30     return MAXSum;
 31 }











问题的提出:给定有n个整数(可能为负整数)组成的序列a1,a2,...,an,求该序列连续的子段和的最大值。如果该序列的所有元素都是负整数时定义其最大子段和为0。

例如,当(a1,a2,a3,a4,a5)=(-5,11,-4,13,-4-2)时,最大子段和为11+(-4)+13=20。

 

解法一:穷举法,即把所有可能情况一一列举

穷举法是最直接的想法,把所有的情况列出来,再进行挑选。

同样是穷举法,下面两个写法优劣就不一样。有的人可能还会增加空间开销,使用一个数组来保存结果。

1)使用三层循环

下面的算法是这样的:(使用字典序的方式)从序列a[]的第一个开始,算a[0]的和,算a[0]~a[1]的和,算a[0]~a[2]的和

……算a[0]~a[n-1]的和,然后算a[1]的和,算a[1]~a[2]的和,算a[1]~a[3]的和,一直算到a[n-2]~a[n-1]的和、

算a[n-1]的和,在每次计算a[i]~a[j]的和后,都要和当前最大子段和sum比较,若发现更大的,就更新sum的值。

前两层循环就是完成字典序穷举,而第三层循环是计算a[i]~a[j]的和。


  1. //begin,end分别记录最大子段和的开始和结尾位置的下标,下标从0开始  
  2. //a[]是待求数组,n是序列长度  
  3. int maxSum(int a[],int n,int &begin,int &end){  
  4.     int sum=0;//用来保存最大子段和的值  
  5.     for (int i=0;i<n;i++)  
  6.         for(int j=i;j<n;i++){  
  7.             int temSum=0;//temSum保存每一次a[i]~a[j]的和,然后和当前最大子段和比较  
  8.             for(int k=i;k<=j;k++)  
  9.                 temSum+=a[k];//计算a[i]~a[j]的和  
  10.             if(temSum>sum){//如果发现更大的子段和,则更新sum的值,并保存当前最大子段和的开始和结尾下标  
  11.                 sum=temSum;  
  12.                 begin=i;  
  13.                 end=j;  
  14.             }  
  15.         }  
  16.     return sum;  
  17. }  


这算法很清晰,就是挨个列举,如果发现有比sum更大的值,就更新sum。但是重复做了很多工作,导致时间复杂度为O(n^3),每一次计算a[i]~a[j]的和都要从a[i]一直累加至a[j],其实我们是可以先保存a[i]~a[j-1]的和至一个变量temSum,那么a[i]~a[j]的和就等于temSum+a[j],这就是下面两层循环的写法

2)使用两层循环

  1. int maxSum(int a[],int n,int &begin,int &end){  
  2.     int sum=0;//用来保存最大子段和的值  
  3.     for(int i=0;i<n;i++){  
  4.         int temSum=0;//保存从下表为i开始至j的和,当求a[i]~a[j+1]的和时,就可以变为求temSum+a[j+1]  
  5.         for(int j=i;j<n;i++){  
  6.             temSum+=a[j];  
  7.             if(temSum>sum){  
  8.                 sum=temSum;  
  9.                 begin=i;  
  10.                 end=j;  
  11.             }  
  12.         }  
  13.     }  
  14.     return sum;   
  15. }  


可以看到,保存了a[i]~a[j-1]和的结果后,就可以省去一层循环,时间复杂度也降为O(n^2)。我们在写程序时要根据题目的要求而选择比较省时省空间的写法,这也需要多练习。


解法二:利用分治策略

先要明白分治策略基本思想是把问题规模分解为多个小规模问题,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。一般采用二分法逐步分解(注意,很多算法都用到递归,当然这很耗空间)。分治法解题的一般步骤:

  (1)分解,将要解决的问题划分成若干规模较小的同类问题;

  (2)求解,当子问题划分得足够小时,用较简单的方法解决;

  (3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

本题目总的分治思想是:

如果将所给的序列a[1:n]分为长度相等的两段子序列a[1:n/2]和a[n/2+1:n],分别求出这两段子序列的最大子段和,则总序列的最大子段和有三种情况:1)与前段相同。2)与后段相同。3)跨前后两段。

(我想这解法比较难理解的地方是3)跨前后两段的情况(理解时可以简单列举一个序列按代码执行)。这里注意一下,跨前后两段是指一个连续的子序列跨越前后两段,而不是前后两段最大字段和的简单相加)

具体的分治做法是这样的:先把a[1:n]分成a[1:n/2]和a[n/2+1:n],分别求出两段子序列的最大子段和,而在求a[1:n/2]的最大子段和时,又把a[1:n/2]分成a[1:(n/2)/2]和a[(n/2)/2+1:n/2]两个子序列,照这样一直分,直到把每个子序列都只有一个或两个数未知,当子序列只有一个数时,它的最大子段和要么是自身或为0,而子序列有两个数时,其最大子段和要么为前一个数,要么为后一个数,要么为两个数的和,或者为0(当两个数都为负数时),当返回子序列的最大子段和时,子序列的最大子段和一个数就代表了一个子序列(这点很重要),那么后面每次处理的子序列都是只有或者两个数(因为子序列的最大子段和代表了这个序列)。可以举个例子照着程序执行一下,帮助理解。

 

  1. //left是做端点下标,right是右端点下标  
  2. int maxSubSum(int a[],int left,int right){  
  3.     int sum=0;  
  4.     if(left==right)//这是递归调用必须要有的终值情况。  
  5.         sum=(a[left]>0?a[left]:0);  
  6.     else{  
  7.         int center=(left+right)/2;  
  8.         int leftSum=maxSubSum(a,left,center);//求出左序列最大子段和  
  9.         int rightSum=maxSubSum(a,center+1,right);//求出右序列最大子段和  
  10. ////////////  
  11. //求跨前后两段的情况,从中间分别向两端扩展。  
  12.         //从中间向左扩展。这里注意,中间往左的第一个必然包含在内。  
  13.         int ls=0;int lefts=0;  
  14.         for(int i=center;i>=left;i--){  
  15.             lefts+=a[i];  
  16.             if(lefts>ls)  
  17.                 ls=lefts;  
  18.         }  
  19.         //从中间向右扩展。中间往右的第一个必然包含在内  
  20.         int rs=0;int rights=0;  
  21.         for(i=++center;i<=right;i++){  
  22.             rights+=a[i];  
  23.             if(rights>rs)  
  24.                 rs=rights;  
  25.         }  
  26.         sum=ls+rs;//sum保存跨前后两段情况的最大子段和  
  27. //求跨前后两段的情况完成  
  28. ////////////  
  29.         if(sum<leftSum)  
  30.             sum=leftSum;//记住,leftSum表示前段序列的最大子段和  
  31.         if(sum<rightSum)  
  32.             sum=rightSum;//rightSum表示后段序列的最大字段和  
  33.     }  
  34.     return sum;  
  35. }  


初学者要理解这个算法需要好好去举个例子。解法四的思想或许会对你理解有些帮助。

这个算法的时间复杂度为O(nlogn),分治算法在这主要是作为想法学习用,并不是这道题的最佳算法。
 
解法三:动态规划
先明白动态规划是把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法。建议去百度百科看一下“动态规划"词条里介绍的一些概念,明白它的思想。
本题目总的动态规划思想是这样的:
已知前n个数的最大子段和,那么前n+1个数的最大字段和有两种情况,一是包含前面的结果,二是不包含。
具体做法是这样的,序列a[]有n个数,我们就要做n次决策,从第一个数开始(下标从0开始),假设已经做好了前i个数的决策,并把做第i个数的最大子段和的结果保存到了tem(注意,前i个数的最大子段和sum和第i个数决策的子段和tem是不一样的,前者sum可能不包含第i个数,但第i个数决策的子段和tem一定包含tem,sum是当前最大子段的和,而tem是包含第i个数的子段和,并想办法使tem的值尽可能的大),当做第i+1个数的决策时,要做的工作就只是判断包含第i+1个数的子段和是否要把tem的值包进来,如果tem>0,就包括,否则不包括。
(再看一下总的想法)假设前n个数的最大子段和是tem,在决策前n+1个数的最大子段和时,判断tem的值,如果tem>0,那么前n+1个数的最大子段和为tem加上第n+1个数,否则就是第n+1个数自己。这里记住,你所求的是连续的几个数的和。代码比较简单:
 
  1.    
  2. //begin和end分别表示最大子段和的开始和结束位置的下标,下标从0开始。  
  3. int maxSum(int a[],int n,int &begin,int &end){  
  4.     int sum=0;//sum保存的是当前连续几个数的和的最大值,只是记录目前算得得最大值。  
  5.     int tem=0;//tem表示决策第i个数时所保存的第i-1个数决策状态。  
  6.     for(int i=0;i<n;i++){  
  7.         if(tem>0)  
  8.             tem+=a[i];//如果tem>0,说明tem可  
  9.         else{  
  10.             tem=a[i];  
  11.             begin=i;//如果tem小于等于零,说明重新计算最大字段和,记下开始位置  
  12.         }  
  13.         if(tem>sum){  
  14.             sum=tem;  
  15.             end=i;//如果tem>sum,说明刷新了最大子段和的值,记下结束位置  
  16.         }  
  17.     }  
  18.     return sum;  
  19. }  

只需一次遍历,时间复杂度为O(n),动态规划里有一项很重要的内容就是保存各阶段的状态,有人会增加一个数组保存状态,但写程序可以根据题目要求做些改变,像这道题就只需要保存前一个状态就行。
解法四:
最大子段的左右两个数字必定为正数,最左边数字的左邻是负数,最右边数字的右邻是负数。假设a[i]~a[j]是最大子段和序列的一个子序列,则从a[i-1]逐个往左加,这个和如果在加到a[k]时变成一个正数,那就说明左端点i可以延伸到k,可以使这个子段的和更大一些,右边也同理扩展。我们要做的就是找到最大字段的两个端点。
我们可以先找出从右到左第一个正数作为寻找i的起点(如果一个正数都找不到那显然就是L=0,最大子段和=0),
然后按照上述原理不断向左延拓i;找j也是同理:先找从左到右第一个正数然后向右扩展。把代码贴上来
 
  1. #include <stdio.h>  
  2. #define MAX 100//宏定义要寻找的序列个数最大值  
  3. int fineLeft(int d[],int n);//寻找最大子段的左下标  
  4. int fineRight(int d[],int n);//寻找最大子段的右下标  
  5. int main(){  
  6.     int d[MAX]={0};  
  7.     int n;  
  8.     int i;  
  9.     int left,right;  
  10.     scanf("%d",&n);  
  11.     for (i=0;i<n;i++)  
  12.         scanf("%d",&d[i]);  
  13.     left=fineLeft(d,n);//找出最大子段的左下表  
  14.     if(left<0){//如果left<0,说明没有找到正数  
  15.         printf("0/n");  
  16.         return 0;  
  17.     }  
  18.     right=fineRight(d,n);//找出最大子段的右下标  
  19.     if(left>right){//这种情况应该不会出现。只是保险起见而已。  
  20.         printf("haha/n");  
  21.         return 0;  
  22.     }  
  23.     n=0;//这是我写代码节省空间的一种方式,n下面将保存最大子段和  
  24.     for(i=left;i<=reft;i++)  
  25.         n+=d[i];  
  26.     printf("%d/nbegin=%d,end=%d/n",n,left+1,right+1);  
  27.     return 0;  
  28. }  
  29. int findRight(int d[],int n){  
  30.     int right=0;  
  31.     int sum=0;  
  32.     int i=0;  
  33.       
  34.     while(right<n&&d[right]<=0)//找出第一个正数  
  35.         right++;  
  36.     while(right<n&&i<n){  
  37.         sum=0;  
  38.         for(i=right+1;i<n;i++){  
  39.             sum+=d[i];  
  40.             if(sum>0){//如果加到出现sum>0,说明可以扩展  
  41.                 right=i;//把right定位到i后,继续寻找,看是否还能扩展  
  42.                 break;  
  43.             }  
  44.         }  
  45.     }  
  46.     return right;  
  47. }  
  48. int fineLeft(int d[],int n){  
  49.     int left=n-1;  
  50.     int sum=0;  
  51.     int i=0;  
  52.     while(left>=0&&d[left]<=0)  
  53.         left--;  
  54.     while (left>=0&&i>=0){  
  55.         sum=0;  
  56.         for (i=left-1;i>=0;i--){  
  57.             sum+=d[i];  
  58.             if (sum>0){  
  59.                 left=i;  
  60.                 break;  
  61.             }  
  62.         }  
  63.     }  
  64.     return left;  
  65. }  


//这里的扩展思想可以帮助理解分治策略的第三种情况(从中间往两边扩展)。
本方法至多只需三次遍历,一次往左,一次往右,一次最大子段求和。时间复杂度为O(n)。不过写法应该还可以再改进,找左右端点的函数应该可以抽象出一个模型。
转自:http://blog.youkuaiyun.com/zhong36060123/article/details/4381391
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值