1. 程式人生 > >演算法分析初步-最大連續和問題(二)

演算法分析初步-最大連續和問題(二)

本節使用剛學到的分治法來解決這個問題。再來回顧一下這個演算法:

分治演算法一般分為如下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友好等。