【題目描述】洛谷1115最大連續子序列求和
給出一段序列,選出其中連續且非空的一段使得這段和最大。
【輸入格式】
第1行是一個正整數N(N≤200000),表示了序列的長度。
第2行包含N個絕對值不大於10000的整數A[i],描述了這段序列。
【輸出格式】
輸出僅包括1個整數,爲最大的子段和是多少。子段的最小長度爲1。
解題方案:
- 暴力枚舉O(n3)
- 枚舉——優化O(n2)
- 枚舉——再優化O(n)
- 分治O(nlog2n)
- 動態規劃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只有三種關係:
- [x,y]完全處於mid左邊,即x≤y<mid
- [x,y]橫跨mid,即x<mid<y
- [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);
}
總結
- 一個題目從不同的角度出發,可能會有不同的解法。
- 一種算法是否可行,不一定要實現,通過分析就能知道大概。
- 有時候正解可能是在某個暴力算法的基礎上優化而來(解法3),這個優化可能是某種思想(極大,極小,倍增,差分),也有可能需要採用一切特殊的數據結構(堆或 優先隊列,單調隊列,樹狀數組,線段樹,ST表)。