问题的提出:给定有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;
}
初学者要理解这个算法需要好好去举个例子。解法四的思想或许会对你理解有些帮助。
//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;
}
#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;
}