Algorithms 最大子串行和

2021-08-07 17:08:08 字數 4133 閱讀 4380

**:

最大連續子數列和一道很經典的演算法問題,給定乙個數列,其中可能有正數也可能有負數,我們的任務是找出其中連續的乙個子數列(不允許空數列),使它們的和盡可能大。我們一起用多種方式,逐步優化解決這個問題。

為了更清晰的理解問題,首先我們先看一組資料:

8

-2 6 -1 5 4 -7 2 3

第一行的8是說數列的長度是8,然後第二行有8個數字,即待計算的數列。

對於這個數列,我們的答案應該是14,所選的數列是從第2個數到第5個數,這4個數的和是所有子數列中最大的。

暴力求解也是容易理解的做法,簡單來說,我們只要用兩層迴圈列舉起點和終點,這樣就嘗試了所有的子數列,然後計算每個子數列的和,然後找到其中最大的即可,c語言**如下:

#include //n是陣列長度,num是待計算的陣列,放在全域性區是因為可以開很大的陣列

int n, num[1024];

int main()

if(s > ans) ans = s;}}

printf("%d\n", ans);

return 0;

}

這個演算法的時間複雜度是o(n^3),複雜度的計算方法可參考《演算法導論》第一章,如果我們的計算機可以每秒計算一億次的話,這個演算法在一秒內只能計算出500左右長度數列的答案。

如果你讀懂了剛才的程式,我們可以來看乙個簡單的優化。

如果我們有這樣乙個陣列sum,sum[i]表示第1個到第i個數的和。那麼我們如何快速計算第i個到第j個這個數列的和?對,只要用sum[j] - sum[i-1]就可以了!這樣的話,我們就可以省掉最內層的迴圈,讓我們的程式效率更高!c語言**如下:

#include //n是陣列長度,num是待計算的陣列,sum是陣列字首和,放在全域性區是因為可以開很大的陣列

int n, num[16384], sum[16384];

int main()

int ans = num[1]; //ans儲存最大子數列和,初始化為num[1]能保證最終結果正確

//i和j分別是列舉的子數列的起點和終點

for(int i = 1; i <= n; i++)

}printf("%d\n", ans);

return 0;

}

這個演算法的時間複雜度是o(n^2)。如果我們的計算機可以每秒計算一億次的話,這個演算法在一秒內能計算出10000左右長度數列的答案,比之前的程式已經有了很大的提公升!此外,我們在這個程式中建立了乙個sum陣列,事實上,這也是不必要的,我們我就也可以把陣列字首和直接計算在num陣列中,這樣可以節約一些記憶體。

你應該聽說過分治法,正是:分而治之。我們有乙個很複雜的大問題,很難直接解決它,但是我們發現可以把問題劃分成子問題,如果子問題規模還是太大,並且它還可以繼續劃分,那就繼續劃分下去。直到這些子問題的規模已經很容易解決了,那麼就把所有的子問題都解決,最後把所有的子問題合併,我們就得到複雜大問題的答案了。可能說起來簡單,但是仍不知道怎麼做,接下來分析這個問題:

首先,我們可以把整個數列平均分成左右兩部分,答案則會在以下三種情況中:

1、所求數列完全包含在左半部分的數列中。

2、所求數列完全包含在右半部分的數列中。

3、所求數列剛好橫跨分割點,即左右數列各佔一部分。

前兩種情況和大問題一樣,只是規模小了些,如果三個子問題都能解決,那麼答案就是三個結果的最大值。我們主要研究一下第三種情況如何解決:

我們只要計算出:以分割點為起點向左的最大連續數列和、以分割點為起點向右的最大連續數列和,這兩個結果的和就是第三種情況的答案。因為已知起點,所以這兩個結果都能在o(n)的時間複雜度能算出來。

遞迴不斷減小問題的規模,直到數列長度為1的時候,那答案就是數列中那個數字。

綜上所述,c語言**如下,遞迴實現:

#include //n是陣列長度,num是待計算的陣列,放在全域性區是因為可以開很大的陣列

int n, num[16777216];

int solve(int left, int right)

sum = 0;

for(int i = mid + 1; i <= right; i++)

//答案是三種情況的最大值

int ans = lmax + rmax;

if(lans > ans) ans = lans;

if(rans > ans) ans = rans;

return ans;

}int main()

不難看出,這個演算法的時間複雜度是o(n*logn)的(想想歸併排序)。它可以在一秒內處理百萬級別的資料,甚至千萬級別也不會顯得很慢!這正是演算法的優美之處。對遞迴不太熟悉的話可能會對這個演算法有所疑惑,那可就要仔細琢磨一下了。

很多動態規劃演算法非常像數學中的遞推。我們如果能找到乙個合適的遞推公式,就能很容易的解決問題。

我們用dp[n]表示以第n個數結尾的最大連續子數列的和,於是存在以下遞推公式:

dp[n] = max(0, dp[n-1]) + num[n]

仔細思考後不難發現這個遞推公式是正確的,則整個問題的答案是max(dp[m]) | m∈[1, n]。c語言**如下:

#include //n是陣列長度,num是待計算的陣列,放在全域性區是因為可以開很大的陣列

int n, num[134217728];

int main()

printf("%d\n", ans);

return 0;

}

這裡我們沒有建立dp陣列,根據遞迴公式的依賴關係,單獨乙個num陣列就足以解決問題,建立乙個一億長度的陣列要占用幾百mb的記憶體!這個演算法的時間複雜度是o(n)的,所以它計算一億長度的數列也不在話下!不過你如果真的用乙個這麼大規模的資料來測試這個程式會很慢,因為大量的時間都耗費在程式讀取資料上了!

考慮我們之前o(n^2)的演算法,即乙個簡單的優化一節,我們還有沒有辦法優化這個演算法呢?答案是肯定的!

我們已知乙個sum陣列,sum[i]表示第1個數到第i個數的和,於是sum[j] - sum[i-1]表示第i個數到第j個數的和。

那麼,以第n個數為結尾的最大子數列和有什麼特點?假設這個子數列的起點是m,於是結果為sum[n] - sum[m-1]。並且,sum[m]必然是sum[1],sum[2]...sum[n-1]中的最小值!這樣,我們如果在維護計算sum陣列的時候,同時維護之前的最小值, 那麼答案也就出來了!為了節省記憶體,我們還是只用乙個num陣列。c語言**如下:

#include //n是陣列長度,num是待計算的陣列,放在全域性區是因為可以開很大的陣列

int n, num[134217728];

int main()

printf("%d\n", ans);

return 0;

}

看起來我們已經把最大連續子數列和的問題解決得很完美了,時間複雜度和空間複雜度都是o(n),不過,我們確實還可以繼續!

很顯然,解決此問題的演算法的時間複雜度不可能低於o(n),因為我們至少要算出整個數列的和,不過如果空間複雜度也達到了o(n),就有點說不過去了,讓我們把num陣列也去掉吧!

#include int main()

printf("%d\n", ans);

return 0;

}

這個程式的原理和另闢蹊徑,又乙個o(n)的演算法中介紹的一樣,在計算字首和的過程中維護之前得到的最小值。它的時間複雜度是o(n),空間複雜度是o(1),這達到了理論下限!唯一比較麻煩的是ans的初始化值,不能直接初始化為0,因為數列可能全為負數!

至此,最大連續子數列和的問題已經被我們完美解決!然而以上介紹的演算法都只是直接求出問題的結果,而不能求出具體是哪乙個子數列,其實搞定這個問題並不複雜,具體怎麼做留待讀者思考吧!

其他:

最大子串行

模板 int maxsubsequence const int a,int n return maxsum 複雜度on 給定k個整數的序列,其任意連續子串行可表示為,其中 1 i j k。最大連續子串行是所有連續子串行中元素和最大的乙個,例如給定序列,其最大連續子串行為,最大和 為20。在今年的資料...

最大子串行

最大子串行是要找出由數組成的一維陣列中和最大的連續子串行。比如的最大子串行就是 它的和是8,達到最大 而 的最大子串行是,它的和是6。你已經看出來了,找最大子串行的方法很簡單,只要前i項的和還沒有小於0那麼子串行就一直向後擴充套件,否則丟棄之前的子串行開始新的子串行,同時我們要記下各個子串行的和,最...

最大子串行

maxsum最大子串行問題。其實是動態規劃問題,遞推式如下 s 1 a 1 s n s n 1 0?s n 1 a n a n 注意點 暴力是可以解決問題,但是時間肯定會超時。暴力就是把所有子串行都查一遍然後找乙個最大的。如序列為123456,它的所有子串行為 1 2 3 4 5 6 12 123 ...