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

来源:互联网 发布:个体营业执照 网络经营 编辑:程序博客网 时间:2024/04/30 08:04

问题的提出:给定有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]的和。


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


这算法很清晰,就是挨个列举,如果发现有比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)使用两层循环

int maxSum(int a[],int n,int &begin,int &end){int sum=0;//用来保存最大子段和的值for(int i=0;i<n;i++){int temSum=0;//保存从下表为i开始至j的和,当求a[i]~a[j+1]的和时,就可以变为求temSum+a[j+1]for(int j=i;j<n;i++){temSum+=a[j];if(temSum>sum){sum=temSum;begin=i;end=j;}}}return sum;}


可以看到,保存了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(当两个数都为负数时),当返回子序列的最大子段和时,子序列的最大子段和一个数就代表了一个子序列(这点很重要),那么后面每次处理的子序列都是只有或者两个数(因为子序列的最大子段和代表了这个序列)。可以举个例子照着程序执行一下,帮助理解。

 

//left是做端点下标,right是右端点下标int maxSubSum(int a[],int left,int right){int sum=0;if(left==right)//这是递归调用必须要有的终值情况。sum=(a[left]>0?a[left]:0);else{int center=(left+right)/2;int leftSum=maxSubSum(a,left,center);//求出左序列最大子段和int rightSum=maxSubSum(a,center+1,right);//求出右序列最大子段和//////////////求跨前后两段的情况,从中间分别向两端扩展。//从中间向左扩展。这里注意,中间往左的第一个必然包含在内。int ls=0;int lefts=0;for(int i=center;i>=left;i--){lefts+=a[i];if(lefts>ls)ls=lefts;}//从中间向右扩展。中间往右的第一个必然包含在内int rs=0;int rights=0;for(i=++center;i<=right;i++){rights+=a[i];if(rights>rs)rs=rights;}sum=ls+rs;//sum保存跨前后两段情况的最大子段和//求跨前后两段的情况完成////////////if(sum<leftSum)sum=leftSum;//记住,leftSum表示前段序列的最大子段和if(sum<rightSum)sum=rightSum;//rightSum表示后段序列的最大字段和}return sum;}


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

这个算法的时间复杂度为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个数自己。这里记住,你所求的是连续的几个数的和。代码比较简单:
 
 //begin和end分别表示最大子段和的开始和结束位置的下标,下标从0开始。int maxSum(int a[],int n,int &begin,int &end){int sum=0;//sum保存的是当前连续几个数的和的最大值,只是记录目前算得得最大值。int tem=0;//tem表示决策第i个数时所保存的第i-1个数决策状态。for(int i=0;i<n;i++){if(tem>0)tem+=a[i];//如果tem>0,说明tem可else{tem=a[i];begin=i;//如果tem小于等于零,说明重新计算最大字段和,记下开始位置}if(tem>sum){sum=tem;end=i;//如果tem>sum,说明刷新了最大子段和的值,记下结束位置}}return sum;}

只需一次遍历,时间复杂度为O(n),动态规划里有一项很重要的内容就是保存各阶段的状态,有人会增加一个数组保存状态,但写程序可以根据题目要求做些改变,像这道题就只需要保存前一个状态就行。
解法四:
最大子段的左右两个数字必定为正数,最左边数字的左邻是负数,最右边数字的右邻是负数。假设a[i]~a[j]是最大子段和序列的一个子序列,则从a[i-1]逐个往左加,这个和如果在加到a[k]时变成一个正数,那就说明左端点i可以延伸到k,可以使这个子段的和更大一些,右边也同理扩展。我们要做的就是找到最大字段的两个端点。
我们可以先找出从右到左第一个正数作为寻找i的起点(如果一个正数都找不到那显然就是L=0,最大子段和=0),
然后按照上述原理不断向左延拓i;找j也是同理:先找从左到右第一个正数然后向右扩展。把代码贴上来
 
#include <stdio.h>#define MAX 100//宏定义要寻找的序列个数最大值int fineLeft(int d[],int n);//寻找最大子段的左下标int fineRight(int d[],int n);//寻找最大子段的右下标int main(){int d[MAX]={0};int n;int i;int left,right;scanf("%d",&n);for (i=0;i<n;i++)scanf("%d",&d[i]);left=fineLeft(d,n);//找出最大子段的左下表if(left<0){//如果left<0,说明没有找到正数printf("0/n");return 0;}right=fineRight(d,n);//找出最大子段的右下标if(left>right){//这种情况应该不会出现。只是保险起见而已。printf("haha/n");return 0;}n=0;//这是我写代码节省空间的一种方式,n下面将保存最大子段和for(i=left;i<=reft;i++)n+=d[i];printf("%d/nbegin=%d,end=%d/n",n,left+1,right+1);return 0;}int findRight(int d[],int n){int right=0;int sum=0;int i=0;while(right<n&&d[right]<=0)//找出第一个正数right++;while(right<n&&i<n){sum=0;for(i=right+1;i<n;i++){sum+=d[i];if(sum>0){//如果加到出现sum>0,说明可以扩展right=i;//把right定位到i后,继续寻找,看是否还能扩展break;}}}return right;}int fineLeft(int d[],int n){int left=n-1;int sum=0;int i=0;while(left>=0&&d[left]<=0)left--;while (left>=0&&i>=0){sum=0;for (i=left-1;i>=0;i--){sum+=d[i];if (sum>0){left=i;break;}}}return left;}


//这里的扩展思想可以帮助理解分治策略的第三种情况(从中间往两边扩展)。
本方法至多只需三次遍历,一次往左,一次往右,一次最大子段求和。时间复杂度为O(n)。不过写法应该还可以再改进,找左右端点的函数应该可以抽象出一个模型。
原创粉丝点击