算法分析初步-最大连续和问题(二)

版权声明:转载请注明出处。 https://blog.csdn.net/baidu_38304645/article/details/83684703

本节使用刚学到的分治法来解决这个问题。再来回顾一下这个算法:

分治算法一般分为如下3个步骤。

划分问题:把问题的实例划分成子问题。

递归求解:递归解决子问题。

合并问题:合并子问题的解得到原问题的解。

在本例中,划分就是把序列分成元素个数尽量相等的两半:递归求解就是分别求出完全位于左半或者完全位于右半的最佳序列:合并就是求出起点位于左半、终点位于右半的最大连续和序列,并和子问题的最优解比较。

前两部分没有什么特别之处,关键在于合并步骤,既然起点位于左半。终点位于右半。则可以人为的把这样的序列分成两部分,然后独立求解:先寻找最佳起点,然后再寻找最佳终点

int maxsum3(int *A,int x,int y)
{
    //分治
    //把问题划分成子问题
    //递归解决子问题
    //合并子问题的解得到原问题的解
    //O(nlogn)
    int i,v,L,R,m,maxs;
    if(y-x==1) //只有一个元素,直接返回
        return A[x];
    m=x+(y-x)/2;  //第一步 划分成[x,m)和[m,y) /取整为向0取整 相比(x+y)/2可以保证靠近起点!
    maxs=max(maxsum3(A,x,m),maxsum3(A,m,y)); //第二步 递归求解
    v=0; //第三步 合并(1) 从分界点开始往左的最大连续和L
    L=A[m-1];
    for(i=m-1;i>=x;i--)
        L=max(L,v+=A[i]);
    v=0; //第三步 合并(2) 从分界点开始往右的最大连续和R
    R=A[m];
    for(i=m;i<y;i++)
        R=max(R,v+=A[i]);
    return max(maxs,L+R);//把子问题的解与L和R比较
}

上面的代码用到了 复制运算本身具有返回值的特点。在一定程度上简化了代码,但不会牺牲可读性。

在上面的程序中,L和E分别为从分界线往左、往右能达到的最大连续和。对于n=1000,tot值仅为9976,在前面的O(n^{2})算法基础上又有大幅度改进。

是否可以像前面那样,得到tot的数学表达式呢?注意求和技巧已经不再适用。需要用递归的思路进行分析:设序列长度为n时的tot值为T(n),则T(n)=2T(n/2) + n, T(1) = 1.其中2T(n/2)是两次长度为n/2的递归调用,而最后的n是合并的时间(整个序列恰好扫描一遍)。注意这个方程是近似的,因为当n为奇数时两次递归的序列长度分别为(n-1)/2和(n+1)/2,而不是n/2。幸运的是,这样的近似对于最终结果影响很小,在分析算法时总是可以忽略它。

解刚才的方程,可以得到T(n) = \Theta (nlogn).由于nlogn增长很慢,当n扩大两倍时,运行时间的扩大倍数只是略大于2。

在结束对分治算法的讨论之前,有必要再谈谈上述程序中的两个细节。首先是范围表示,上面的程序用左闭右开区间来表示一个范围,好处是在处理数组分割时比较自然:区间[x,y)被分成的是[x,m)和[m,y),不需要在任何地方加减1,另外,空区间表示为[x,x),比[x,x-1]顺眼多了。

另一个细节是分成元素个数尽量相等的两半时分界点的计算。在数学上,分界点应当是x和y的平均数 m=(x+y)/2,此处却用的是 x+(y-x)/2.

在数学上两者相等,但在计算机中却有差别。不知读者是否注意到,运算符 “/” 的 取整是朝零方向的取整,而不是向下取整。换句话说,5/2的值是2,而-5/2的值是-2。为了方便分析,此处用x+(y-x)/2来确保分界点总是靠近区间起点。这在本题中并不是必要的,但在二分查找中,确实相当重要的技巧(此处存疑,在二分中好像也没啥用?一说是防止整数溢出)。

对于最大连续和问题,本篇先后介绍了时间复杂度为 O(n^{3}),O(n^{2}),O(nlogn)的算法,每个新算法较前一个来说,都是重大的改进。尽管分治法看上去很巧妙,但并不是最高效的。把O(n^2)算法稍作修改,便可以得到一个O(n)算法:当j确定时,S[j] - S[i-1]最大相当于 S[i-1]最小,因此只需要扫描一次数组,维护目前遇到过的最小S即可。

int maxsum4(int *A)
{
   //优化 连续子序列之和等于前缀和之差 当j确定时,
   //S[j]-S[i-1]最大便是相当于S[i-1]最小,只要维护
   //目前遇到的最小S即可
   //O(n)
   int i,j,best,S[maxn],mins;
   best=A[1];
   mins=S[0]=0;
   for(i=1;i<=n;i++)
      S[i]=S[i-1]+A[i]; //递推前缀和S
   for(i=1;i<=n;i++)
   {
       mins=min(mins,S[i-1]); //j之前的最小的S
       best=max(best,S[i]-mins); //获得当前的最大值
   }
   return best;
}

渐进时间复杂为多项式的算法称为多项式时间算法,也称有效算法:而n!或者2^n这样的低效的算法称为指数时间算法

不过需要注意的是,上界分析的结果在趋势上能反应算法的效率,但有两个不精确性:一是公式本身的不精确性。例如,“非主流”基本操作的影响、隐藏在大O记号后的低次项和最高项系数:二是对程序实现细节与计算机硬件的依赖性。例如,对复杂表达式的优化计算,把内存访问方式设计得更加cache友好等。

猜你喜欢

转载自blog.csdn.net/baidu_38304645/article/details/83684703