1. 程式人生 > >最大子段和問題詳解(51Nod

最大子段和問題詳解(51Nod

N個整陣列成的序列a[1],a[2],a[3],…,a[n],求該序列如a[i]+a[i+1]+…+a[j]的連續子段和的最大值。當所給的整數均為負數時和為0。

例如:-2,11,-4,13,-5,-2,和最大的子段為:11,-4,13。和為20。

輸入

第1行:整數序列的長度N(2 <= N <= 50000)
第2 - N + 1行:N個整數(-10^9 <= A[i] <= 10^9)

輸出

輸出最大子段和。

輸入示例

6
-2
11
-4
13
-5
-2


輸出示例

20

看見這個問題你的第一反應是用什麼演算法? 

(1) 列舉?對,列舉是萬能的!列舉什麼?子陣列的位置!好列舉一個開頭位置i,一個結尾位置j>=i,再求a[i..j]之間所有數的和,找出最大的就可以啦。好的,時間複雜度?


(1.1)列舉i,O(n)
(1.2)列舉j,O(n)
(1.3)求和a[i..j],O(n)

大概是這樣一個計算方法:

for(int i = 1; i <= n; i++)
{
    for(int j = i; j <= n; j++)
    {
        int sum = 0;
        for(int k = i; k <= j; k++)
            sum += a[k];
            
        max = Max(max, sum);
    }
}

所以是O(n^3), 複雜度太高?降低一下試試看?

(2) 仍然是列舉! 能不能在列舉的同時計算和?
(2.1)列舉i,O(n)
(2. 2)列舉j,O(n) ,這裡我們發現a[i..j]的和不是a[i..j – 1]的和加上a[j]麼?所以我們在這裡當j增加1的時候把a[j]加到之前的結果上不就可以了麼?對!所以我們毫不費力地降低了複雜度,得到了一個新地時間複雜度為O(n^2)的更快的演算法。

程式碼如下:

#include<bits/stdc++.h>
using namespace std;
int a[50002];

int main()
{
	int n;
	long long Max=a[1];
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
	{
		long long sum=0;
		for(int j=i;j<=n;j++)
		{
			sum+=a[j];
			Max=max(Max,sum);
		}
	}
	if(Max>0)
		printf("%lld\n",Max);
	else 
		printf("0\n");
	return 0;
}

是不是到極限了?遠遠不止!


(3)分治一下?


我們從中間切開陣列,原陣列的最大子段和要麼是兩個子陣列的最大子段和(分), 要麼是跨越中心分界點的最大子段和(合)。 那麼跨越中心分界點的最大子段合怎麼計算呢?仍然是列舉! 從中心點往左邊找到走到哪裡可以得到最大的合,再從中心點往右邊檢查走到哪裡可以得到最大的子段合,加起來就可以了。可見原來問題之所以難,是因為我們不知道子陣列從哪裡開始,哪裡結束,沒有“著力點”,有了中心位置這個“著力點”,我們可以很輕鬆地通過迴圈線性時間找到最大子段和。


於是演算法變成了


(3.1)拆分子陣列分別求長度近乎一半的陣列的最大子段和sum1, sum2


時間複雜度 2* T(n / 2)


(3.2)從中心點往兩邊分別分別找到最大的和,找到跨越中心分界點的最大子段和sum3 時間複雜度 O(n)


那麼總體時間複雜度是T(n) = 2 * T(n / 2) + O(n) = O(nlogn), 又優化了一大步,不是嗎?

AC程式碼如下:

#include<bits/stdc++.h>
typedef long long ll;
int a[100005];
ll maxsub(ll left,ll right)
{
	int center=(left+right)/2;
	if(left==right)
	{
		if(a[left]>0)	return a[left];
		else return 0;
	}
	else
	{
		ll left_sum,right_sum;
		left_sum=maxsub(left,center);
		right_sum=maxsub(center+1,right);
		ll sum=0;
		ll max=0;
		ll right_max=0,left_max=0;
		for(int i=center;i>=left;i--)
		{
			sum+=a[i];
			if(left_max<sum)	left_max=sum;
		}
		sum=0;
		for(int i=center+1;i<=right;i++)
		{
			sum+=a[i];
			if(right_max<sum)	right_max=sum;
		}
		max=right_max+left_max;
		if(max<left_sum)	max=left_sum;
		if(max<right_sum)	max=right_sum;
		return max;
	}
}

int main()
{
	int n;
	scanf("%d",&n);
	for(int i=0;i<n;i++)
		scanf("%d",&a[i]);
	printf("%lld\n",maxsub(0,n-1));
}

還能優化嗎?再想想,別放棄!

我們在解法(3)裡需要一個“著力點”達到O(n)的子問題時間複雜度,又在解法(2)裡輕易地用之前的和加上一個新的元素得到現在的和,那麼“之前的和”有那麼重要麼?如果之前的和是負數呢?顯然沒用了吧?我們要一段負數的和,還不如從當前元素重新開始了吧?


再想想,如果我要選擇a[j],那麼“之前的和”一定是最大的並且是正的。不然要麼我把“之前的和”換成更優,要麼我直接從a[j]開始,不是更好麼?


動態規劃大顯身手。我們記錄dp[i]表示以a[i]結尾的全部子段中最大的和。我們看一下剛才想到的,我取不取a[i – 1],如果取a[i – 1]則一定是取以a[i – 1]結尾的子段和中最大的一個,所以是dp[i – 1]。 那如果不取dp[i – 1]呢?那麼我就只取a[i]孤零零一個好了。注意dp[i]的定義要麼一定取a[i]。 那麼我要麼取a[i – 1]要麼不取a[i -1]。 那麼那種情況對dp[i]有利? 顯然取最大的嘛。所以我們有dp[i] = max(dp[i – 1] + a[i], a[i]) 其實它和dp[i] = max(dp[i – 1] , 0) + a[i]是一樣的,意思是說之前能取到的最大和是正的我就要,否則我就不要!初值是什麼?初值是dp[1] = a[1],因為前面沒的選了。


那麼結果是什麼?我們要取的最大子段和必然以某個a[i]結尾吧?那麼結果就是max(dp[i])了。


這樣,我們的時間複雜度是O(n),空間複雜度也是O(n)——因為要記錄dp這個陣列。

#include<bits/stdc++.h>
#include<cstring>
using namespace std;
typedef long long ll;
ll a[100005];
ll dp[100005];

int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%lld",&a[i]);
	memset(dp,0,sizeof(dp));
	dp[1]=a[1];
	int MAX=1;
	for(int i=2;i<=n;i++)
	{
		dp[i]=max(dp[i-1],(long long)0)+a[i];
		if(dp[i]>dp[MAX])	MAX=i;
	}
	printf("%lld\n",dp[MAX]);
}

演算法達到最優了嗎? 好像是!還可以優化!我們注意到dp[i] = max(dp[i - 1], 0) + a[i], 看它只和dp[i – 1]有關,我們為什麼要把它全記錄下來呢?為了求所有dp[i]的最大值?不,最大值我們也可以求一個比較一個嘛。


我們定義endmax表示以當前元素結尾的最大子段和,當加入a[i]時,我們有endmax’ = max(endmax, 0) + a[i], 然後再順便記錄一下最大值就好了。

虛擬碼如下;(陣列下標從1開始)

endmax = answer = a[1]
for i = 2 to n do
    endmax = max(endmax, 0) + a[i]
    answer = max(answer, endmax)
endfor

時間複雜度?O(n)!空間複雜度?O(1)! 簡單吧?我們不僅優化了時間複雜度和空間複雜度,還使程式碼變得簡單明瞭,更不容易出錯。


老生常談的問題來了。我們如何找到一個這樣的子段?請看上面的為虛擬碼endmax = max(endmax, 0) + a[i], 對於endmax它對應的子段的結尾顯然是a[i],我們怎麼知道這個子段的開頭呢? 就看它有沒有被更新。也就是說如果endmax’ = endmax + a[i]則對應子段的開頭就是之前的子段的開頭。否則,顯然endmax開頭和結尾都是a[i]了,讓我們來改一下虛擬碼:

start = 1
answerstart = asnwerend = 1
endmax = answer = a[1]
for end = 2 to n do
	if endmax > 0 then
		endmax += a[end]
	else
		endmax = a[end]
		start = end
	endif
	if endmax > answer then
		answer = endmax
		answerstart = start
		answerend = end
	endif
endfor

這裡我們直接用end作為迴圈變數,通過更新與否決定start是否改變。


總結:通過不斷優化,我們得到了一個時間複雜度為 O(n),空間複雜度為O(1)的簡單的動態規劃演算法。動態規劃,就這麼簡單!優化無止境!

相關推薦

HDU acm 1003 Max Sum || 動態規劃求大子序列

line namespace num more sequence mem ould 動態規劃 ger Max Sum Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Ot

大子問題51Nod

N個整陣列成的序列a[1],a[2],a[3],…,a[n],求該序列如a[i]+a[i+1]+…+a[j]的連續子段和的最大值。當所給的整數均為負數時和為0。 例如:-2,11,-4,13,-5,-2,和最大的子段為:11,-4,13。和為20。 輸入 第1行:整

大子

最大 負數 nbsp 端點 關於 一段 描述 計數器 曾經 題目名稱:最大子段和 題目描述:給出一段序列,選出其中連續且非空的一段使得這段和最大。 輸入格式: 第一行是一個正整數N,表示了序列的長度。 第2行包含N個絕對值不大於10000的整數A[i],描述了這段序列。

51Nod 1050 迴圈陣列大子dp)

 其實就是求迴圈陣列的最大欄位和 題意:給定一個長度為50000的陣列,求它的迴圈陣列的最大子段和。 分析:本題與普通的最大子段和問題不同的是,最大子段和可以是首尾相接的情況,即可以迴圈。那麼

C++學習1):大子多種解法)

多少 問題: code namespace 數據 組成 amp using () 問題:給定由n個數(可能為負數)組成的序列a1,a2,a3,...,an,求該序列子段和的最大值。 第一種解法:(最容易考慮的方法,將所有的子段一一相加,然後比較) 1 #include&

51Nod 1050 循環數組大子 | DP

urn F12 int ges href 中間 art space style Input示例 6 -2 11 -4 13 -5 -2 Output示例 20 分析: 有兩種可能,第一種為正常從[1 - n]序列中的最大子字段和;第二種為數組的total_sum -

51nod 1050 循環數組大子【環形DP/大子/正難則反】

pre 不但 spa 個數 ace lld 時間 lin bsp 1050 循環數組最大子段和 基準時間限制:1 秒 空間限制:131072 KB 分值: 10 難度:2級算法題 收藏 關註 N個整數組成的循環序列a[1],a[2

SP1043 GSS1 - Can you answer these queries I線段樹,區間大子靜態))

有一種 nbsp 不用 端點 合並 表示 格式 space iostream 題目描述 給出了序列A[1],A[2],…,A[N]。 (a[i]≤15007,1≤N≤50000)。查詢定義如下: 查詢(x,y)=max{a[i]+a[i+1

codevs 3981 動態大子線段樹)

輸入 typedef fault namespace 一行 scrip img sum spl 題目傳送門:codevs 3981 動態最大子段和 題目描述 Description 題目還是簡單一點好... 有n個數,a[1]到a[n]。 接下來q次查詢,每次動

[SHOI2015]腦洞治療儀惡心的線段樹,區間大子

由於 得到 \n define 範圍 ret scan 定義 add 題目描述: 曾經發明了自動刷題機的發明家 SHTSC 又公開了他的新發明:腦洞治療儀——一種可以治療他因為發明而日益增大的腦洞的神秘裝置。 為了簡單起見,我們將大腦視作一個 01 序列。11代表這個位置的

【SHOI2015】腦洞治療儀惡心的線段樹,區間大子

-i string 修改 def 由於 返回 系列 lazy long 題目描述: 曾經發明了自動刷題機的發明家 SHTSC 又公開了他的新發明:腦洞治療儀——一種可以治療他因為發明而日益增大的腦洞的神秘裝置。為了簡單起見,我們將大腦視作一個 01

51Nod1050 迴圈陣列大子動態規劃)

這題區間是可以迴圈的,如果不迴圈的狀態轉移方程是 if(dp[i-1]>0)   dp[i]=dp[i-1]+a[i]; else   dp[i]=a[i]; 現在題目要求是可以迴圈,分為兩種情況: 1、沒有迴圈,找到了最大的子段。 2、迴圈了,找到了最大的子段。 第一

大子長遞增子序列貪心與動態規劃)

話不多說先上程式碼。。。。。  最大子段和 題目描述 給出一段序列,選出其中連續且非空的一段使得這段和最大。 輸入輸出格式 輸入格式:   第一行是一個正整數NNN,表示了序列的長度。 第二行包含NNN個絕對值不大於100001000010000的

51nod 1094 迴圈陣列大子

解題思路:本問題跟之前和為k的連續區間解題演算法稍有不同: 1.該題連續區間的範圍是迴圈序列; 2.如果仍然使用上述演算法會超時。 解題方法: 對於迴圈序列,需要考慮兩種情況: 1.不考慮迴圈序列的情況下求出連續序列的最大值 2.若是最大值的產生情況是一部

備戰Noip2018模擬賽11B組) T1 Maxsum 大子

10月27日備戰Noip2018模擬賽11(B組) T1 Maxsum最大子段和 題目描述 給出一個首尾相連的迴圈序列,從中找出連續的一段,使得該段中的數和最大。 輸入格式 第一行一個整數n ,表示有n 個數。 第二行有ñ 個整數。 輸出格式 只一個

51nod 1049 大子

題目描述 N個整陣列成的序列a[1],a[2],a[3],…,a[n],求該序列如a[i]+a[i+1]+…+a[j]的連續子段和的最大值。當所給的整數均為負數時和為0。 例如:-2,11,-4,13,-5,-2,和最大的子段為:11,-4,13。和為20。 輸

大子問題java描述)

最大子段和問題: 1.問題描述:最大子段和問題就是:給定由n個整數(可能為負數)組成的序列a1,a2,a3......an,求該序列形如ai+ai+1+ai+2+...+aj的子段的最大和。當所有的整數均為負數時,定義其最大子段和為0,。例如,序列(a1,a2,a3,a4,a5,a6)=(-

poj 1050 To the Max動態規劃處理二維大子

2、題目大意: 給一個N,然後給定一個N*N的二維陣列,然後求一個子矩陣,使得其中的數加起來和最大 3、思路: 將二維陣列轉換成一維陣列,假設二維陣列是M行N列,那麼將二維陣列分成N條,用dp[i]記錄第i列的和(可以是任意連續長度,for迴圈就能實現),那麼將dp[i]

51Nod 1051 大子矩陣 (大子變形)

基準時間限制:2 秒 空間限制:131072 KB 分值: 40 難度:4級演算法題  收藏  關注 一個M*N的矩陣,找到此矩陣的一個子矩陣,並且這個子矩陣的元素的和是

求區間大子線段樹)

填坑。。。 線段樹需要維護的是: 左端點 x 右端點 y (本人喜歡直接維護端點) [x,y]內的最大子段和 ms [x,y]的區間和 s [x,y]內的緊靠左端點的最大子段和 ls [x,y]內的緊靠右端點的最大子段和 rs 困難就是,upd