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

编程者都希望自己的算法高效,但算法在写成程序之前是运行不了的,难道每设计出一个算法都必须写出程序才能知道快不快吗?答案是否定的。本节介绍算法分析的基本概念和方法,力求在编程之前尽量准确地估计程序的时空开销,并作出决策-例如,如果算法又复杂速度又慢,就不要急着写出来了。

给出一个长度为n的序列A1,A2,...,An,求最大连续和,换句话说,要求找找到1 <= i <= j <= n,使得Ai + Ai+1 + ... + Aj尽量大。

输入:序列的大小n,序列的各个元素Ai。

输出:最大的连续和。

运行结果:

我们最容易想到的方法就是枚举了。

int maxsum1(int *A)
{
    //O(n^3)
    int i,j,k,sum,best;
    int tot = 0;
    best=A[1];
    for(i=1;i<=n;i++)
        for(j=i;j<=n;j++)       //检查连续子序列A[i],...A[j]
        {
            sum=0;
            for(k=i;k<=j;k++)
            {
                sum+=A[k];
                tot++;
            }
            best=max(best,sum); //更新最大值
        }
    return best;
}

注意best的初值是A[1],这是最保险的做法-不要写best=0(也许序列中全为负数)。当n=1000时,输出tot=167167000,这时加法运算的次数当n=50时,输出22100.

为什么要计算tot呢?因为它与机器的运行速度无关。不同机器的速度不一样,运行时间也会有所差异,但tot一定相同,换句话说,它去了机器相关的因素,只衡量算法的工作量大小-具体来说,是加法操作的次数。

在本题中,将加法操作作为基本操作,类似地也可以把其他四则运算、比较运算作为基本操作,一般并不会严格定义基本操作的类型,而是根据不同情况灵活处理。

刚才是实验得出tot值的,起始它也可以用数学方法直接推导出,设输入规模为n时加法操作的次数为T(n),则:

 T(n) = \sum_{i=1}^{n}\sum_{j=i}^{n}j - i + 1 = \sum_{i=1}^{n}\frac{(n-i+1)(n-i+2)}{2}=\frac{n(n+1)(n+2)}{6}

上面的公式是关于n的三次多项式,意味着当n很大时,平方项和一次项对整个多项式值的影响不大。可以用一个记号来表示:T(n) = \Theta (n^{3}),或者说T(n)与n^{3}同阶。同阶是什么意思呢?简单的说,就是增长情况相同,前面说过,n很大时,只有立方项起到决定作用,而立方项的系数对增长是不起作用的-n扩大两倍时,n^3和100n^3都扩大8倍,这样一来,可以只保留最大项,并忽略其系数,得到的简单式子称为算法的渐进时间复杂度

读者可以做个实验,看看n扩大两倍时运行时间是否近似扩大8倍,注意这里的8倍是近似的,因为在T(n)的表达式中,二次项、一次项和常数项都被忽略掉了;程序中的其他运算,如if(sum > best)中的比较运算,甚至改变循环变量所需的自增都没有考虑在内。

尽管如此,算法分析的效果还是比较精确的,因为抓住了主要矛盾-执行得最多的运算是加法。

对于上面的方法,读者可能会有疑问:难道每次都要做一番复杂的数学推导才能得到渐进时间复杂度么?当然不必。

下面是另一种推导方法:算法包含3重循环,内层最坏情况下需要循环n次,中层循环最坏情况下也需要n次,外层循环最坏情况下仍然需要n次,因此总运算次数不超过n^{3}.这里采用了上界分析,假定所有最坏情况同时取到,尽管这时不可能的。不难预料,这样的分析和实际情况肯定会有一定偏差-在T(n)的表达式中,n^3的系数是1/6,小于n^3,但数量级是正确的-仍然可以得到 n扩大两倍时,运行时间近似扩大8倍的结论,上界也有记号T(n) = O(n^{3})

松的上界也是正确的上界,但可能让人过高估计程序运行的实际时间(从而不敢编写程序),而即使上界是紧的,过大(如100)或过小(如1/100)的最高项系数同样可能引起错误的估计,换句话说,算法分析不是万能,要谨慎对待分析结果。如果预感到上界不紧,系数过大或者过小,最好还是要编程实践。

下面试着优化一下这个算法,设Si = A1 + A2 + .. +Ai,则,Ai + Ai+1 + .... Aj = Sj - Si-1.该式子的用途相当广泛,其直观含义是“连续子序列之和等于两个前缀和只差”。有了这个结论,最内层的循环就可以省略了。

int maxsum2(int *A)
{
   //优化 连续子序列之和等于前缀和之差
   //O(n^2)
   int i,j,best,S[maxn];
   best=A[1];
   S[0]=0;
   for(i=1;i<=n;i++)
      S[i]=S[i-1]+A[i]; //递推前缀和S
   for(i=1;i<=n;i++)
      for(j=i;j<=n;j++)
         best=max(best,S[j]-S[i-1]); //更新最大值
   return best;
}

注意上面的程序用到了递推的思想:从小到大依次计算S[1],S[2],S[3],....每个只需要在前一个基础上加上一个元素,换句话说,计算S这个步骤的时间复杂度为O(n)。接下来是一个二重循环,用类似的方法可以分析出:

T(n) = \sum_{i=1}^{n}n-i+1 = \frac{n(n+1)}{2}

代入可得T(1000)=500500,和运行结果一致。同样的,用上界分析可以更快的得到结论:内层循环最坏情况下要执行n次,外层也是,因此时间复杂度为O(n^{2}).

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值