经典算法问题 - 最大连续子数列和
轉(zhuǎn)載自:https://www.cnblogs.com/conw/p/5896155.html
最大連續(xù)子數(shù)列和一道很經(jīng)典的算法問題,給定一個數(shù)列,其中可能有正數(shù)也可能有負(fù)數(shù),我們的任務(wù)是找出其中連續(xù)的一個子數(shù)列(不允許空序列),使它們的和盡可能大。我們一起用多種方式,逐步優(yōu)化解決這個問題。
為了更清晰的理解問題,首先我們先看一組數(shù)據(jù):
8
-2 6 -1 5 4 -7 2 3
第一行的8是說序列的長度是8,然后第二行有8個數(shù)字,即待計算的序列。
對于這個序列,我們的答案應(yīng)該是14,所選的數(shù)列是從第2個數(shù)到第5個數(shù),這4個數(shù)的和是所有子數(shù)列中最大的。
最暴力的做法,復(fù)雜度O(N^3)
暴力求解也是容易理解的做法,簡單來說,我們只要用兩層循環(huán)枚舉起點和終點,這樣就嘗試了所有的子序列,然后計算每個子序列的和,然后找到其中最大的即可,C語言代碼如下:
#include <stdio.h>//N是數(shù)組長度,num是待計算的數(shù)組,放在全局區(qū)是因為可以開很大的數(shù)組 int N, num[1024];int main() {//輸入數(shù)據(jù)scanf("%d", &N);for(int i = 1; i <= N; i++)scanf("%d", &num[i]);int ans = num[1]; //ans保存最大子序列和,初始化為num[1]能保證最終結(jié)果正確//i和j分別是枚舉的子序列的起點和終點,k所在循環(huán)計算每個子序列的和for(int i = 1; i <= N; i++) {for(int j = i; j <= N; j++) {int s = 0;for(int k = i; k <= j; k++) {s += num[k];}if(s > ans) ans = s;}}printf("%d\n", ans);return 0; }?
這個算法的時間復(fù)雜度是O(N^3),復(fù)雜度的計算方法可參考《算法導(dǎo)論》第一章,如果我們的計算機(jī)可以每秒計算一億次的話,這個算法在一秒內(nèi)只能計算出500左右長度序列的答案。
一個簡單的優(yōu)化
如果你讀懂了剛才的程序,我們可以來看一個簡單的優(yōu)化。
如果我們有這樣一個數(shù)組sum,sum[i]表示第1個到第i個數(shù)的和。那么我們?nèi)绾慰焖儆嬎愕趇個到第j個這個序列的和?對,只要用sum[j] - sum[i-1]就可以了!這樣的話,我們就可以省掉最內(nèi)層的循環(huán),讓我們的程序效率更高!C語言代碼如下:
?
這個算法的時間復(fù)雜度是O(N^2)。如果我們的計算機(jī)可以每秒計算一億次的話,這個算法在一秒內(nèi)能計算出10000左右長度序列的答案,比之前的程序已經(jīng)有了很大的提升!此外,我們在這個程序中創(chuàng)建了一個sum數(shù)組,事實上,這也是不必要的,我們我就也可以把數(shù)組前綴和直接計算在num數(shù)組中,這樣可以節(jié)約一些內(nèi)存。
換個思路,繼續(xù)優(yōu)化
你應(yīng)該聽說過分治法,正是:分而治之。我們有一個很復(fù)雜的大問題,很難直接解決它,但是我們發(fā)現(xiàn)可以把問題劃分成子問題,如果子問題規(guī)模還是太大,并且它還可以繼續(xù)劃分,那就繼續(xù)劃分下去。直到這些子問題的規(guī)模已經(jīng)很容易解決了,那么就把所有的子問題都解決,最后把所有的子問題合并,我們就得到復(fù)雜大問題的答案了。可能說起來簡單,但是仍不知道怎么做,接下來分析這個問題:
首先,我們可以把整個序列平均分成左右兩部分,答案則會在以下三種情況中:
1、所求序列完全包含在左半部分的序列中。
2、所求序列完全包含在右半部分的序列中。
3、所求序列剛好橫跨分割點,即左右序列各占一部分。
前兩種情況和大問題一樣,只是規(guī)模小了些,如果三個子問題都能解決,那么答案就是三個結(jié)果的最大值。我們主要研究一下第三種情況如何解決:
我們只要計算出:以分割點為起點向左的最大連續(xù)序列和、以分割點為起點向右的最大連續(xù)序列和,這兩個結(jié)果的和就是第三種情況的答案。因為已知起點,所以這兩個結(jié)果都能在O(N)的時間復(fù)雜度能算出來。
遞歸不斷減小問題的規(guī)模,直到序列長度為1的時候,那答案就是序列中那個數(shù)字。
綜上所述,C語言代碼如下,遞歸實現(xiàn):
?
不難看出,這個算法的時間復(fù)雜度是O(N*logN)的(想想歸并排序)。它可以在一秒內(nèi)處理百萬級別的數(shù)據(jù),甚至千萬級別也不會顯得很慢!這正是算法的優(yōu)美之處。對遞歸不太熟悉的話可能會對這個算法有所疑惑,那可就要仔細(xì)琢磨一下了。
動態(tài)規(guī)劃的魅力,O(N)解決!
很多動態(tài)規(guī)劃算法非常像數(shù)學(xué)中的遞推。我們?nèi)绻苷业揭粋€合適的遞推公式,就能很容易的解決問題。
我們用dp[n]表示以第n個數(shù)結(jié)尾的最大連續(xù)子序列的和,于是存在以下遞推公式:
dp[n] = max(0, dp[n-1]) + num[n]
仔細(xì)思考后不難發(fā)現(xiàn)這個遞推公式是正確的,則整個問題的答案是max(dp[m]) | m∈[1, N]。C語言代碼如下:
這里我們沒有創(chuàng)建dp數(shù)組,根據(jù)遞歸公式的依賴關(guān)系,單獨一個num數(shù)組就足以解決問題,創(chuàng)建一個一億長度的數(shù)組要占用幾百MB的內(nèi)存!這個算法的時間復(fù)雜度是O(N)的,所以它計算一億長度的序列也不在話下!不過你如果真的用一個這么大規(guī)模的數(shù)據(jù)來測試這個程序會很慢,因為大量的時間都耗費在程序讀取數(shù)據(jù)上了!
另辟蹊徑,又一個O(N)的算法
考慮我們之前O(N^2)的算法,即一個簡單的優(yōu)化一節(jié),我們還有沒有辦法優(yōu)化這個算法呢?答案是肯定的!
我們已知一個sum數(shù)組,sum[i]表示第1個數(shù)到第i個數(shù)的和,于是sum[j] - sum[i-1]表示第i個數(shù)到第j個數(shù)的和。
那么,以第n個數(shù)為結(jié)尾的最大子序列和有什么特點?假設(shè)這個子序列的起點是m,于是結(jié)果為sum[n] - sum[m-1]。并且,sum[m]必然是sum[1],sum[2]...sum[n-1]中的最小值!這樣,我們?nèi)绻诰S護(hù)計算sum數(shù)組的時候,同時維護(hù)之前的最小值, 那么答案也就出來了!為了節(jié)省內(nèi)存,我們還是只用一個num數(shù)組。C語言代碼如下:
?
看起來我們已經(jīng)把最大連續(xù)子序列和的問題解決得很完美了,時間復(fù)雜度和空間復(fù)雜度都是O(N),不過,我們確實還可以繼續(xù)!
大道至簡,最大連續(xù)子序列和問題的完美解決
很顯然,解決此問題的算法的時間復(fù)雜度不可能低于O(N),因為我們至少要算出整個序列的和,不過如果空間復(fù)雜度也達(dá)到了O(N),就有點說不過去了,讓我們把num數(shù)組也去掉吧!
#include <stdio.h>int main() {int N, n, s, ans, m = 0;scanf("%d%d", &N, &n); //讀取數(shù)組長度和序列中的第一個數(shù)ans = s = n; //把a(bǔ)ns初始化為序列中的的第一個數(shù)for(int i = 1; i < N; i++) {if(s < m) m = s;scanf("%d", &n);s += n;if(s - m > ans)ans = s - m;}printf("%d\n", ans);return 0; }?
這個程序的原理和另辟蹊徑,又一個O(N)的算法中介紹的一樣,在計算前綴和的過程中維護(hù)之前得到的最小值。它的時間復(fù)雜度是O(N),空間復(fù)雜度是O(1),這達(dá)到了理論下限!唯一比較麻煩的是ans的初始化值,不能直接初始化為0,因為數(shù)列可能全為負(fù)數(shù)!
至此,最大連續(xù)子序列和的問題已經(jīng)被我們完美解決!然而以上介紹的算法都只是直接求出問題的結(jié)果,而不能求出具體是哪一個子序列,其實搞定這個問題并不復(fù)雜,具體怎么做留待讀者思考吧!
轉(zhuǎn)載于:https://www.cnblogs.com/woolsen/p/9315825.html
總結(jié)
以上是生活随笔為你收集整理的经典算法问题 - 最大连续子数列和的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Hibernate学习(二)
- 下一篇: MP实战系列(十一)之封装方法详解(续一