最大連續子序列求和的多種解法

題目鏈接

【題目描述】洛谷1115最大連續子序列求和
給出一段序列,選出其中連續且非空的一段使得這段和最大。
【輸入格式】
第1行是一個正整數N(N≤200000),表示了序列的長度。
第2行包含N個絕對值不大於10000的整數A[i],描述了這段序列。
【輸出格式】
輸出僅包括1個整數,爲最大的子段和是多少。子段的最小長度爲1。

解題方案:

  1. 暴力枚舉O(n3)
  2. 枚舉——優化O(n2)
  3. 枚舉——再優化O(n)
  4. 分治O(nlog2n)
  5. 動態規劃O(n)

對於此問題的求解與優化,讓人不得不折服於算法的魅力我就是這麼入坑的

1.暴力枚舉

大多數題目不出意外第一想法基本都是枚舉;對於這個題目的暴力其實不難想到,題目求解的是一個連續區間的和,區間必然有左右端點,因此直接枚舉左端點L和右端點R,知道區間了求和還不是分分鐘的事(循環累加唄),代碼大概長得如下:

for(int L=1;L<=n;L++)//枚舉左端點
	for(int R=L;R<=n;R++){//枚舉右端點
		int s=0;
		for(int i=L;i<=R;i++)//求和L到R之間的數字的和
			s+=arr[i];
		ans=max(ans,s);
	}

其實很多算法不需要實現,通過時空複雜度分析即可;不難看出該算法時間複雜度高達O(n3),,當n規模超過1000就會TLE。不過該算法還可以通過“前綴和”優化。

2.枚舉——“前綴和”優化

前綴和:與數組arr聲明一個一樣大的數組sum,將arr的前i項和保存到sum[i],不難得到sum[i]=sum[i-1]+arr[i]
例如:

1 2 3 4 5 6 7 8
arr 1 3 -3 4 2 -2 7 -3
sum 1 4 1 5 7 5 12 9

應用前綴和求解arr[ L到R ]的和:
sum[R]-sum[L-1] = arr[L]+arr[L+1]+……+arr[R]
因此,可以先預處理出前綴和,然後用sum[R]-sum[L-1]代替for L to R求和;可優化到O(n2)。分析後發現該時間複雜度依然不能滿足題目要求,當然前綴和思想還是不錯的,代碼大概如下:

sum[0]=0;
for(int i=1;i<=n;i++)sum[i]=sum[i-1]+arr[i];
for(int L=1;L<=n;L++)
	for(int R=L;R<=n;R++)
		ans=max(ans,sum[R]-sum[L-1]);

枚舉算法至此,似乎已經山窮水盡呢,因爲區間必然有左右端點,似乎最少也得兩個循環,也就是說最少也得O(n2)。
事實上,並非如此,在前綴和的基礎上,可以再優化。

3.枚舉——再優化

再上述算法中不難看出核心代碼就一句:

ans=max(ans,sum[R]-sum[L-1])

如果我們固定R,那麼這句話可以理解爲,找到一個最好的左端點,使得最終的和最大。
顯然,R固定後,sum[R]是不會改變的,因此要使sum[R]-sum[L-1]儘量大,只要找到最小的sum[L-1]即可。
因此,枚舉右端點R,並維護一個前R項的最小和mins即可。可省去枚舉左端點的循環,從而將算法優化到O(n)。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int sum[N],ans=-N,n,mins=0;
int main(){
	scanf("%d",&n);
	for(int R=1;R<=n;R++){
		scanf("%d",&sum[R]);
		sum[R]+=sum[R-1];
		ans=max(ans,sum[R]-mins);
		mins=min(mins,sum[R]);//維護前R項的最小和
	}
	printf("%d",ans);
	return 0;
} 

4.分治

分治思想:大事化小,小事化了。

假設要求解的區間爲[L,R],L,R的重點爲mid,而且最大和的那一段爲[x,y],則[x,y]與mid只有三種關係:

  1. [x,y]完全處於mid左邊,即x≤y<mid
  2. [x,y]橫跨mid,即x<mid<y
  3. [x,y]完全位於mid右邊,midx<x≤y

對於第1種和第3種情況,顯然是一個子問題。大問題是求解[L,R]中的[x,y],情況1和情況3是求解[L,mid]和[mid+1,R]中的[x,y];因此屬於規模更小的子問題,直接遞歸求解即可。
再看情況2,橫跨mid,則表明最優解一定是mid左邊一部分,右邊一部分,因此只需要求出以mid結尾的最優左子段Lsum,以及mid+1的最優右字段Rsum,Lsum+Rsum就是第2種情況的最優解。

最後,返回三種情況中的最大值即可。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+6;
int arr[N],n;
int getMax(int l,int r){
	if(l==r)return arr[l];
	int mid=(l+r)/2,lsum=-2e9,rsum=-2e9;
	for(int i=mid,sum=0;i>=l;i--)//查找以arr[mid]結尾的左段最大和 
		sum+=arr[i],lsum=max(lsum,sum);
	for(int i=mid+1,sum=0;i<=r;i++)//查找以arr[mid+1]開頭的右段最大和 
		sum+=arr[i],rsum=max(rsum,sum);
	return max(max(getMax(l,mid),lsum+rsum),getMax(mid+1,r));//取三種情況的最大
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&arr[i]);
	printf("%d",getMax(1,n));
	return 0;
}

5.動態規劃DP

動態規劃求解此題是一個經典算法,決策也很簡單,因爲對於數字arr[i],它只有兩種選擇:

  • 連接到前面的序列的末尾,形成一個更長的序列
  • 不連接到前面的序列,自己成爲一個新序列的開頭

狀態方程也比較容易,f(i)表示以第i個數結尾的最大連續子序列的 和
對於數字i的決策:

  • 若f(i-1)>0,顯然將arr[i]連接到前面的序列更划算
  • 若f(i-1)<0,不連接到前面的序列更划算

狀態轉移方程:f(i)=max(f[i-1],0)+arr[i]
只需要掃描一次arr[i],因此時間複雜度爲O(n),當然仔細分析會發現數組arr其實以不要。因爲只用了一次,完全可以直接用f[i]代替arr[i]

#include<cstdio>
const int N=2e5+6;
int f[N],n,ans=-2E+9;;
int main(){
	scanf("%d",&n);	
	for(int i=1;i<=n;i++){
		scanf("%d",&f[i]);
		f[i]+=max(0,f[i-1]);
		ans=max(f[i],ans);
	}
	printf("%d",ans);
}

總結

  1. 一個題目從不同的角度出發,可能會有不同的解法。
  2. 一種算法是否可行,不一定要實現,通過分析就能知道大概。
  3. 有時候正解可能是在某個暴力算法的基礎上優化而來(解法3),這個優化可能是某種思想(極大,極小,倍增,差分),也有可能需要採用一切特殊的數據結構(堆或 優先隊列,單調隊列,樹狀數組,線段樹,ST表)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章