连续最大和 |
问题描述
简单的连续最大和就是给你一个数组,求其子序列的最大和是多少。比如[1, -1, 2, -1, 3, -2],很明显答案是 2 + (-1) + 3 = 4
。
例题:连续最大和
问题变形
- 如HDU1003 Max Sum,不仅要求出连续最大和,还要输出这个区间下标。上述样例中,答案区间可以是[1, 5],也可以是[3, 5],具体输出看题目要求
- 第二种变形是给你一个矩阵,要求一个子矩阵和最大(NYOJ104-最大和)
- 第三种变形是将原数组复制m次,求拼接后的数组连续最大和(2020.9.6字节跳动笔试),n和m都是1e5级别
解决方案
原始问题解决方案很多
- 方案一: 可以用线段树,将前缀和扔进线段树中,然后查询区间最小值,作差。但是线段树代码量稍微大了些。
- 方案二: 基于上述思路,还可以用一个变量记录前缀和的最小值,然后用当前的前缀和减去这个值即可。
- 方案三: 在第二种的方案的基础上改进,用一个变量记录和,如果这个变量小于0,用当前的值重新赋值,否则继续累加
- 坑点: 看清题目要求,是否至少选一个,比如所有数都是负数,我们初始化如果为0这样就无法选择了。
以牛客例题为例
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const double eps=1e-8;
const int INF=1e9+7;
const int MOD=1e9+7;
const int N=1e6+10;
int n,m;
int main()
{
while(~scanf("%d",&n))
{
ll x;
ll ans=-1e10,t=0;
ll sum=0;
for(int i=1;i<=n;i++)
{
scanf("%lld",&x);
/* 方案二,此题无法通过,因为必须要选一个
sum+=x;
t=min(t,sum);
ans=max(ans,sum-t);
*/
// 方案三
if(t<0) t=x;
else t+=x;
ans=max(ans,t);
}
cout<<ans<<endl;
}
return 0;
}
字节跳动那道笔试题数据通过率90%,可能没有意识到必须要选一个,这也是一个很隐蔽的坑点。
那道题对m分情况即可,以及对前缀和sum[n]分情况即可。
其实完全不用管m的。
- 先求出只有一个数组时的连续最大和:ans1
- 如果
sum[n]<0
,拼接再多也无益,只会越加越小,这种情况就拼接一次即可,当然m需要大于等于2,拼接一次求连续最大和很简单,或者求原数组的后缀最大值和拼接一次数组的前缀最大值求和:ans2 - 如果
sum[n]>=0
,那么我们只需要特殊处理首尾两个数组即可,中间的全是完整的数组对答案的贡献是(m-2)*sum[n]
,也要注意m是否大于等于2,首尾数组的处理同上,首数组的最大后缀加上尾部数组的最大前缀,这样就拼接起来了:ans2 - max(ans1,ans2)就是最大值
放一下我写的代码吧,只过了90%的数据,原题中a[i]的数据范围是[-1e6, 1e6],可能存在全为负的情况吧。
const int N=1e6+10;
ll dp[N];
ll sum[N];
int n,m;
int main()
{
while(~scanf("%d%d",&n,&m))
{
sum[0]=0;
ll ans1=0,ans2=0,ans3=0,tmp=0;
for(int i=1; i<=n; i++)
{
scanf("%lld",&dp[i]);
sum[i]=sum[i-1]+dp[i];
tmp=min(tmp,sum[i]);
ans1=max(ans1,sum[i]-tmp); // 中
ans2=max(ans2,sum[i]); // 前
}
for(int i=n; i>=1; i--)
ans3=max(ans3,sum[n]-sum[i-1]); // 后
if(sum[n]>=0)
{
if(m>2)
{
ans3+=max(0,m-2)*sum[n];
ans3=max(ans3,ans3+ans2);
}
else if(m==2)
ans3=max(ans3,ans3+ans2);
}
else
{
if(m>=2)
ans3=max(ans3,ans3+ans2);
}
cout<<max(ans1,ans3)<<endl;
}
return 0;
}
//5 3
//1 3 -9 2 4
单调栈应用 |
什么是单调栈,就是用栈来维护一个单调的序列
序列用数组存起来,将位置扔进栈中,每次比较当前元素与栈顶位置的元素。
典型应用,求两侧第一个大于等于或者小于等于当前数的值或位置。
例题一:POJ2559 Largest Rectangle in a Histogram
求最大的木板面积
这个题我们求出每个值两侧比当前值小的位置即可。
typedef long long ll;
const int N=1e5+10;
int n,a[N];
ll l[N],r[N];
int main()
{
while(~scanf("%d",&n)&&n)
{
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
for(int i=1; i<=n; i++)
scanf("%d",&a[i]);
stack<int>q;
int i=2;
q.push(1);
while(i<=n)
{
if(!q.empty()&&a[i]<a[q.top()])
{
l[q.top()]=i-1;
q.pop();
}
else
q.push(i++);
}
while(!q.empty())
{
l[q.top()]=i-1;
q.pop();
}
i=n-1;
q.push(n);
while(i>=1)
{
if(!q.empty()&&a[i]<a[q.top()])
{
r[q.top()]=i+1;
q.pop();
}
else
q.push(i--);
}
while(!q.empty())
{
r[q.top()]=i+1;
q.pop();
}
// for(int i=1;i<=n;i++)
// printf("%lld%c",l[i],i==n?'\n':' ');
// for(int i=1;i<=n;i++)
// printf("%lld%c",r[i],i==n?'\n':' ');
ll ans=0;
for(int i=1; i<=n; i++)
ans=max(ans,(l[i]-r[i]+1)*a[i]);
cout<<ans<<endl;
}
return 0;
}
例题二:超级码力在线编程大赛初赛 第1场 - 3.大楼间穿梭
如题,每次跳跃可以跳到右边第一个大于等于当前数的位置上。
问题转化为如何求出右侧第一个大于等于当前数的位置。用单调栈维护即可,从左往右扫描数组,将位置扔进栈中,栈中的元素表示需要确定右侧位置的元素,如果当前元素比栈顶元素大或等,那么栈顶元素的右侧要找的第一个位置就是当前位置,重复判断直到栈为空;将当前位置也扔进栈中。
class Solution {
public:
/**
* @param heights: the heights of buildings.
* @param k: the vision.
* @param x: the energy to spend of the first action.
* @param y: the energy to spend of the second action.
* @return: the minimal energy to spend.
*/
typedef long long ll;
const static ll INF=1e15+7;
const static int N=1e6+10;
int vis[N],head[N],tot;
ll d[N];
struct node
{
int v;
ll cost;
friend bool operator<(node A,node B)
{
return A.cost>B.cost;
}
};
struct Edge
{
int to,next;
ll cost;
} e[N*2];
priority_queue<node>q;
void init()
{
tot=0;
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
}
void add(int u,int v,int w)
{
e[tot].to=v,e[tot].cost=w,e[tot].next=head[u];
head[u]=tot++;
}
void add_jump(vector<int> &a,int n,int x,int k)
{
stack<int>tmp;
tmp.push(0);
int i=1;
while(i<n)
{
if(!tmp.empty()&&a[i]>=a[tmp.top()])
{
// res[tmp.top()]=i;
if(i-tmp.top()<=k)
add(tmp.top(),i,x);
tmp.pop();
}
else
tmp.push(i++);
}
}
ll DIJ(int s,int t)
{
while(!q.empty()) q.pop();
for(int i=s;i<=t;i++)
d[i]=i==s?0:INF;
q.push(node{s,d[s]});
while(!q.empty())
{
node TC=q.top();
q.pop();
int u=TC.v;
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i+1;i=e[i].next)
{
int v=e[i].to;
if(d[v]>d[u]+e[i].cost)
{
d[v]=d[u]+e[i].cost;
q.push(node{v,d[v]});
}
}
}
//cout<<d[t]<<endl;
return d[t];
}
long long shuttleInBuildings(vector<int> &heights, int k, int x, int y) {
// write your code here.
init();
int n=heights.size();
for(int i=0; i<n; i++)
{
if(i>0)
add(i-1,i,y);
if(i>1)
add(i-2,i,y);
}
add_jump(heights,n,x,k);
return DIJ(0,n-1);
}
};
字节跳动那道题求每个数两边第一个大于等于当前数的位置,然后进行乘积,求最大值。比如:
5
5 4 3 4 5
输出 8
很简单顺序倒序扫用单调栈找位置即可,不知道哪里错了,数据只通过50%左右。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const double eps=1e-8;
const int INF=1e9+7;
const int MOD=1e9+7;
const int N=1e6+10;
int a[N];
ll b[N],c[N];
int n;
void Get()
{
memset(b,0,sizeof(b));
memset(c,0,sizeof(c));
stack<int>tmp;
tmp.push(1);
int i=2;
while(i<=n)
{
if(!tmp.empty()&&a[i]>a[tmp.top()])
{
b[tmp.top()]=i;
if(i-tmp.top()<=k)
add(tmp.top(),i,x);
tmp.pop();
}
else
{
tmp.push(i);
i++;
}
}
while(!tmp.empty()) tmp.pop();
tmp.push(n);
i=n-1;
while(i>=1)
{
if(!tmp.empty()&&a[i]>a[tmp.top()])
{
c[tmp.top()]=i;
tmp.pop();
}
else
{
tmp.push(i);
i--;
}
}
}
int main()
{
while(~scanf("%d",&n))
{
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
Get();
ll ans=0;
// for(int i=1;i<=n;i++)
// printf("%lld ",b[i]);
// puts("");
// for(int i=1;i<=n;i++)
// printf("%lld ",c[i]);
// puts("");
for(int i=1;i<=n;i++)
ans=max(ans,c[i]*b[i]);
cout<<ans<<endl;
}
return 0;
}
/*
5
5 4 3 4 5
*/