算法分析与优化策略

一、渐进时间复杂度

    基本操作的数量往往可以写成关于“输入规模”的表达式,保留最大项并忽略系数后的简单表达式称为算法的渐进复杂度,用于衡量基本操作数随规模的增长情况。

例如:设输入规模为n时加法操作的次数为T(n),T(n) = n(n+1)(n+2)/6。

当n很大时,平方项和一次项对整个多项式的影响不大,可以用一个记号来表示:T(n) = θ(n3),或者说T(n)和n3同阶。

同阶指“增长情况相同”。

 (渐进时间复杂度忽略了很多因素,因而分析结果只能作为参考,并不是精确的。尽管如此,如果抓住了最主要的运算量所在,算法分析的结果常常十分有用。)

 

二、上界分析

    另一种推导时间渐进复杂度的方法:

下面是求最大连续和的代码

int f(int P[], int n)
{
    int ans = P[0];
    for(int i = 0; i < n; i ++)
    {
        for(int j = i; j < n; j ++)
        {
            int sum = 0;
            for(int k = i; k <= j; k ++)
            {
                sum += P[k];
                if(sum > ans)ans = sum;
            }
        }
    }
    return ans;
}

 3重循环最坏情况下都需要n次,因此总运算次数不超过n3。上界的记号为T(n)=O(n3)。

在算法设计中,常常不进行精确分析,而是假定各种最坏的情况同时取到,得到上界。在很多情况下,这个上界和实际情况同阶(称为“紧”的上界),但也有可能会因为分析方法不够好,得到“松”的上界。

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

ps.最大连续和的一种优化方法:

 

int f(int P[], int S[], int n)
{
    S[0] = 0;
    int ans = P[1];
    for(int i = 1; i <= n; i ++)
    {
        S[i] = S[i-1] + P[i];//S[i]代表从P[1]到P[i]的和
    }
    for(int i = 1; i <= n; i ++)
    {
        for(int j = i; j <= n; j ++)
        {
            ans = max(ans, S[j] - S[i - 1]);
        }
    }
    return ans;
}


int main()
{
    int n;
    cin >> n;
    int P[1000],S[1000];
    for(int i = 1; i <= n; i ++)
    {
        cin >> P[i];
    }
    cout << f(P, S, n) << endl;
    return 0;
}

 

递推思想

用类似的方法可得出T(n)=O(n2)

 

三、分治

根据最大连续和算法的进一步优化,分析分治思想的性能。

 

分治算法一般分为以下3个步骤:

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

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

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

 

最大连续和的分治算法:

1、把序列分成元素个数尽量相等的两半;2、分别求出完全位于左半或者完全位于右半的最佳序列;3、求出起点位于左半、终点位于右半的最大连续和序列,并和子问题的最优解比较。

int f(int A[], int x, int y)//返回在[x,y)中的最大连续和
{
    if(x == y - 1)
    {
        return A[x];
    }
    int mid = x + (y - x) / 2, ans = A[x];
    ans = max(ans, f(A, x, mid));
    ans = max(ans, f(A, mid, y));
    int L = A[mid - 1],R = A[mid];
    for(int i = x; i < mid; i ++)
    {
        int sum = 0;
        for(int j = i; j < mid; j ++)
        {
            sum += A[j];
        }
        L = max(L, sum);
    }
    for(int i = mid; i < y; i ++)
    {
        int sum = 0;
        for(int j = i; j < y; j ++)
        {
            sum += A[j];
        }
        R = max(R, sum);
    }
    ans = max(ans, L + R);
    return ans;
}

   是否可以像前面那样,得到tot(基本操作数)的数学表达式呢?注意求和技巧已经不再适应,需要用递归的思路进行分析:设序列长度为n的tot值为T(n),则T(n)=2T(n/2)+n,T(1)=1。其中2T(n/2)

是2次长度为n/2的递归调用,最后的n是合并的时间。

解方程得T(n)=θ(nlogn)。nlogn增长很慢,比如,当n扩大2倍时,运行时间的扩大倍数只是略大于2。

递归方程T(n)=2T(n/2)+θ(n),T(1)=1的解为T(n)=θ(nlogn)。可以用解答树证明这个理论,也可以作为一个重要结论记下来。

该方法的2个细节:1、左闭右开的“数组分割”。使得处理自然。

2、(x+y)/2和x+(y-x)/2。数学上等价,在计算机中,前者是朝零取整,后者是朝区间起点取整。

 

四、正确对待算法分析结果

假设机器速度是每秒108次基本运算,运算量为n3、n2、nlog2n、n、n!(排列)、2n(子集枚举)的算法,在1s之内能解决最大问题规模n,如表所示:

运算量          n!     2n     n3     n2     nlog2n     n

最大规模        11     26    464   10000   4.5*106    108

速度扩大两倍后  11     27    584   14142   8.6*106    2*108

n!和2n不仅解决的问题规模非常小,而且增长缓慢;nlog2n、n不仅解决问题的规模大,而且增长快。渐进时间复杂为多项式的算法称为多项式时间算法,也称有效算法。像n!和2n 这样低效的算法称为指数时间算法。

需要注意的是,分析结果在趋势上能反映算法的效率,但有2个不确定性:一是公式本身的不精确性(系数、非主流操作的影响);二是对程序实现细节与计算机硬件的依赖性。在不少情况下,算法实际能解决的问题和表所示有着较大差异。

但是还是有一定借鉴意义的,例如n<=8,n!的算法已经足够了,n<=20,需要用到2n的算法,n<=300,必须用至少n3的多项式时间算法。

posted @ 2017-10-16 05:30  哲贤  阅读(865)  评论(0编辑  收藏  举报