1.最大连续和问题
给你一个长度为N的整数序列 {A1,A2,…,An},要求从中找出一段连续的长度不超过M且不为0的子序列,使得这个序列的和最大。
样例输入
6 4
1 -3 5 1 -2 3
样例输出
7
数据范围与提示
1<=N,M<=2e5
当看到这道题的第一眼,直接暴力枚举好吧
然而这道题其实一看就是一个前缀和, 但不是一个简单的前缀和。
WA代码(45分)
#include <bits/stdc++.h>
using namespace std;
const int M = 2e5 + 5;
int a, n, m, ans = INT_MIN, len;
int b[M];
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> a;
if (b[i - 1] > 0)
b[i] = b[i - 1] + a, len++;
else
b[i] = a, len = 1;
if (b[i] > ans && len <= m)
ans = b[i];
}
cout << ans << endl;
return 0;
}
这种做法一眼开过去似乎并没有什么问题,BUT,这样做就有可能会漏掉一些组合。
所以这道题的正解是用双指针。
AC代码
#include <bits/stdc++.h>
using namespace std;
const int M=2e5+5;
int n,m,x,head=1,tail,ans=INT_MIN;
int dp[M],q[M];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>x;
dp[i]=dp[i-1]+x; //dp记录前缀和
}
for(int i=0;i<=n;i++){ //计算以i结尾的最大连续和
while(q[head]+m<i && tail>=head) head++; //保证q[head]与i在m范围之内,q用来记录一段连续子段和的值最小的起点下标
while(dp[i]<dp[q[tail]] && tail>=head) tail--; //从后往前找第一个小于等于dp[i]的位置
if(i!=0) ans=max(ans,dp[i]-dp[q[head]]); //q[head]开始到i的子段和的长度是在m之内的,更新ans
q[++tail]=i; //在tail+1的位置记录i
}
cout<<ans<<endl;
return 0;
}
这样做的话,就是先求出前缀和,再用双指针来求起点和终点。这个q数组就是用来保存一段连续子段和的值最小的起点下标。而i是用来枚举终点。
但需要注意的是head和tail是作为一个相向的,并且要将其作为一个q数组的下标
2.最大平均数子数组
就是给一个数列a_1,a_2,…a_n,求它的一个连续子数列。求长度至少为k,平均值最大的一段子数列的平均值。
为了避免误差,将结果向下取整输出即可。(即 (int)(ans*1000))
样例输入
10 6
6 4 2 10 3 8 5 9 4 1
样例输出
6500
数据范围与提示
1<=N<=100000
-2000<=Ai<=2000
1<=K<=N
这道题的思路就是二分答案,但还是需要用到前缀和。
AC代码
#include <bits/stdc++.h>
using namespace std;
const int M=1e5+5;
const double esp=1e-6;
int n,k;
double a[M],dp[M],f[M];
double l,r,mid;
bool check(double mid){
double ans=-1,mi=2005;
for(int i=1;i<=n;i++){
dp[i]=a[i]-mid; //将平均数减掉,会得到正权值和负权值
f[i]=f[i-1]+dp[i]; //前缀和
}
for(int i=k;i<=n;i++){ //枚举长度>=k的子序列
mi=min(mi,f[i-k]); //mi得到前面一段
ans=max(ans,f[i]-mi); //ans将前面的前缀和减掉后,看是否大于0
}
return ans>=0; //如果ans大于等于0,那就说明答案比它大,移动l
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
l=-1e9,r=1e9;
while(r-l>esp){
mid=(l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
cout<<int(r*1000);
return 0;
}
这个二分找平均数有几个需要注意的点
- 首先,在check函数刚开始时,需要算出前缀和的同时减掉枚举出来的mid,如果和是大于mid就是一个正权值,小于mid就是一个负权值
- 之后,mi变量就是来寻找前缀和里面最小的一个,而那个范围就是从至少的长度k开始。
- ans如果减掉前缀和-mi之后,还是大于等于0的话,那就说明正解还比mid大,所以就可以移动l,否则就只能移动r。
总结
这两道题都是用前缀和加上一些其他的算法来完成的,而这些题都有些共同的特点,比如求最大,但有范围限制。
最后,我有一个提醒,因为我有一个朋友因为这个改了n小时,那就是极值的赋值。
而那个朋友就是@MAGICAL DEER