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

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

程式設計者都希望自己的演算法高效,但演算法在寫成程式之前是執行不了的,難道每設計出一個演算法都必須寫出程式才能知道快不快嗎?答案是否定的。本節介紹演算法分析的基本概念和方法,力求在程式設計之前儘量準確地估計程式的時空開銷,並作出決策-例如,如果演算法又複雜速度又慢,就不要急著寫出來了。

給出一個長度為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}).