引言
在刷算法题时,我们会遇到一种很常见的题目——求解最大子段问题。
即给定由 n 个整数(可能为负整数)组成的序列 a1, a2, ..., an,求该序列的子段和的最大值。当所有整数均为负整数时定义其最大子段和为0。(注意:子段是连续的)。例如,当 (a1, a2, a3, a4, a5, a6) = (-2, 11, -4, 13, -5, -2) 时, 最大子段和为:20。
接下来,我们将探讨两种不同的方法来解决这个问题:一种是直观但效率较低的穷举法,另一种是更高效且体现了递归思维的分治法。
正文
三种代码求解
在代码中,我们定义n是整数序列长度,a是数组名,besti是最大字段的左侧下标,bestj是最大字段的右侧下标。
首先我们放出最好理解的穷举法代码:
int MaxSum(int n, int *a, int &besti, int &bestj){
int sum=0;
for (int i=1; i<=n; i++){
for (int j=i; j<=n; j++){
int thissum=0;
for (int k=i; k<=j; k++){
thissum += a[k];
if (thissum>sum){
sum = thissum;
besti = i;
bestj = j;
}
}
}
}
return sum;
}
可以看到,该算法的时间复杂度为O(n^3)。
for (int i=1; i<=n; i++)
的作用是从前往后遍历数组,使每个数字都可能作为最大序列的左侧数字,然后后面的两层循环便是在假设一个左侧数字后的前提下,遍历获得此种情况的最大字段,for (int j=i; j<=n; j++)
便是通过递增j的值修改字段的长度(长度为i到j),for (int k=i; k<=j; k++)
则是将新加入字段的数字和之前的字段求和。通过三层循环即可遍历得出最大字段的情况。
下面的代码则是优化后的穷举法代码:
int MaxSum(int n, int *a, int &besti, int &bestj){
int sum=0;
for (int i=1; i<=n; i++){
int thissum=0;
for (int j=i; j<=n; j++){
thissum += a[j];
if (thissum>sum){
sum = thissum;
besti = i;
bestj = j;
}
}
}
return sum;
}
这里时间复杂度变成了O(n^2)。
for (int i = 1; i <= n; i++)
的作用没有变化,for (int j=i; j<=n; j++)
则是在i到n中找到“最大字段”(该字段左侧下标是i),这里对上述代码的循环进行了简化。
但是时间复杂度还是有点高,因此我们在分析一下分治法求解。
分治法即将一个父问题分成几个一样的子问题,然后求解子问题,最后汇总子问题的结果得出父问题的结果。
在这里我们的父问题是求解一个序列中的最大字段和,因此我们可以把这个序列从中间分为两个子序列,最后的结果就有了三种情况:①最大字段在左侧序列中。②最大字段在右侧序列中。③最大字段在中间,即包含左右两个序列。
对于前两种情况,我们可以直接递归。第三种情况我们可以先找到“左侧的最大字段”(该字段的右侧数字被限制为中间数字),然后再找到“右侧的最大字段”(该字段的左侧数字被限制为中间数字),然后把两段相加即可。因此我们可以编写出下述代码:
int MaxSubSum(int *a, int left, int right){
int sum=0;
if (left==right)
sum=a[left] > 0 ? a[left]:0;
int center=(left+right)/2;
int leftsum=MaxSubSum(a, left, center);
int rightsum=MaxSubSum(a,center+1,right );
int s1=0; int lefts=0;
for(int i=center; i>=left; i--){
lefts+=a[i];
if (lefts>s1)
s1=lefts;
}
int s2=0; int rights=0;
for(int i=center+1; i<=right; i++){
rights+=a[i];
if (rights>s2)
s2= rights;
}
sum=s1+s2;
if(sum<leftsum) sum=leftsum;
if (sum< rightsum) sum=rightsum;
return sum;
该算法将一个问题划分为两个子问题(即求出左右序列的最大字段),将问题规模n分为n/2,(分解部分代码为:int leftsum=MaxSubSum(a, left, center);int rightsum=MaxSubSum(a,center+1,right );
)。
然后合并时需要从中间数字先遍历到最左侧数字,再从中间数字遍历到最右侧数字,即遍历了整个序列,所以合并的时间复杂度为O(n)。
因此可以建立递归方程:
根据公式法可以得出此递归方程的解的渐进阶为:T(n)=O(nlogn)。
公式法
分治法的时间复杂性所满足的递归关系,即一个规模为 n 的问题被分为规模均为 n/b 的 a 个子问题,递归地求解这 a 个子问题,然后通过对这 a 个子问题的解的综合,得到原问题的解。套用公式如下:
其中:
- T(n) 是解决规模为 n 的问题所需的时间。
- a是每个子问题的数量。
- b 是每个子问题相对于原始问题的比例。
- f(n) 是合并所有子问题结果所需的时间。
最后求解总体时间复杂度则需要将f(n)与进行对比,谁大取谁,相等乘logn。
结语
希望读完本文你有所收获,如有疑问,欢迎留言!