单调队列--求几种类型的最大子段和
在解一个区间的最值问题时,我们可以用到单调队列。
单调队列维护的是区间最值。
1.最大值的维护:
比如我们要维护一个区间为k的最大值的单调队列,由于新插入
的节点他的“生命力”肯定比原先已经在队列中的元素“活”的时间长,将插入元素不断与队尾元素比,
如果他大于队尾元素,那么r--将队尾元素删掉,(因为目前插入的这个元素值(设为pos)更大,而且“活”的时间
长,有pos在,队尾元素的有“生”之年永远都没法为最大值,故而直接无视比pos小的队尾了)。直到对空位置或者
找到了一个比pos大的队尾。
2.K区间的维护:
比如当前k区间的起点为i,即区间为[i,i+k-1],那么如果队头元素front的下标j<i,那么front便不符合
在当前k区间范围内,那么他的值便不属于当前k区间的最值,所以f++将对头出队。这段操作绝对不会遇到队
空的情况,应为第1步已经插入了一个在区间为[i,i+k-1]的元素pos,他下标j必然符合j>=i
【代码】
struct nodes
{
int val,beg ;
};
nodes qu1[N];
int r1,f1 ;
void insert(int m,int id,int L)//L为区间的最左下标
{
while(r1>=f1&&m>qu1[r1].val)
r1--;
qu1[++r1].val=m ;
qu1[r1].beg=id ;
while(qu1[f1].beg<L)
f1++;
}
//f>r qu empty
Init: f=r=0;
insert(a[i],i,L);
poj2823是一个典型的求区间最值的问题。
下面利用单调队列求几种不同类型的最大子段和。(最小子段和也可以转换为最最大子段和,只要全部元素取反就行了)
(1)没有长度限制的最大子段和。
hdu1003 Max Sum
http://acm.hdu.edu.cn/showproblem.php?pid=1003
我们用s[i] 表示a[i]的前i项和 ,s[i]=sum(a[k])(k=1,2,.....i)
然后遍历一遍s[],记录最小的s[i]值,存于min,那么最大子段和就是 max=Max(s[i]-min);
【代码】
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> #include <cmath> #include <vector> using namespace std; int n; const int maxn = 100005; struct node { int val; int id; }q[maxn]; int a[maxn]; int s[maxn]; int main() { int T,ca=1; scanf("%d",&T); while(T--) { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); memset(s,0,sizeof(s)); for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; int mins=0; int minid=0; int ans=-2147483648; int li=0,ri=0; for(int i=1;i<=n;i++) { if(s[i]-mins>ans) { ans=s[i]-mins; ri=i; li=minid+1; } if(s[i]<mins) { mins=s[i]; minid=i; } } printf("Case %d:\n",ca++); printf("%d %d %d\n",ans,li,ri); if(T>0) printf("\n"); } return 0; }
(2)有上界的最大子段和。
【hdu 3415 Max Sum of Max-K-sub-sequence 】
http://acm.hdu.edu.cn/showproblem.php?pid=3415
这里的数列是一个圆,要先预处理一下,变为一条线。
这里多了一点的就是对下标有一点要求,所以我们要维护一个区间长度为k的单调队列。
假设这个区间的长度是 [i,i+k-1];
那么以i+k为结尾的,最大子段和就是 s[i+k]-q[head].(注意head的下标)
注:i+k - [i,i+k-1] <= k
所以我们可以维护当前的k区间最小,方便求下一个节点往回长度不大于k的子段的最大值。
PS:这里的队列队头要预存一个值为sum=0的节点
【代码】
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> #include <cmath> #include <vector> using namespace std; const int maxn = 200005; int n,k; int a[maxn]; int s[maxn]; struct node { int val; int id; }q[maxn]; int main() { int T; scanf("%d",&T); while(T--) { scanf("%d %d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=n+1;i<=2*n;i++) a[i]=a[i-n]; memset(s,0,sizeof(s)); for(int i=1;i<=2*n;i++) s[i]=s[i-1]+a[i]; int ans=-2147483648; int li=0; int ri=0; int head=1; int tail=0; tail+=1; q[tail].val=0; q[tail].id=0; for(int i=1;i<=n+k-1;i++) { if(s[i]-q[head].val>=ans) { if(s[i]-q[head].val==ans && q[head].id+1<li) { ri=i; li=q[head].id+1; } else if(s[i]-q[head].val==ans && (q[head].id+1==li) && (ri-li+1>i-q[head].id)) { ri=i; li=q[head].id+1; } else if(s[i]-q[head].val>ans) { ans=s[i]-q[head].val; li=q[head].id+1; ri=i; } } while(tail>=head && q[tail].val>s[i]) tail-=1; tail+=1; q[tail].val=s[i]; q[tail].id=i; if(i>=k) { while(tail>=head && q[head].id<i-k+1) head+=1; if(q[head].id>=n) break; } } if(ri>n) ri-=n; printf("%d %d %d\n",ans,li,ri); } return 0; }
(3)有上下界的最大子段和
soj2680
这里跟第二种类型也是差不多,我们不断地加入s[]数组,维护一个单调队列。
要注意下标的限制。如果当前插入队列的是s[i],那么我们这时找到的就是以 i+ml+1为结尾的最大子段和。
注意队列头的元素的下标的出队列的问题。
PS:这里的队列队头要预存一个值为sum=0的节点
【代码】
#include <iostream> #include <cstdio> #include <algorithm> #include <cstring> #include <cmath> #include <vector> using namespace std; int n,ml,mu; const int maxn = 40000; int a[maxn]; int s[maxn]; struct node { int val; int id; }q[maxn]; int main() { while(scanf("%d",&n) && n!=0) { scanf("%d %d",&ml,&mu); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); a[i]=-a[i]; } memset(s,0,sizeof(s)); for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; int ans=s[ml]; int head=1; int tail=0; tail+=1; q[tail].val=0; q[tail].id=0; for(int i=ml+1;i<=n;i++) { while(tail>=head && q[tail].val>s[i-ml]) tail-=1; tail+=1; q[tail].val=s[i-ml]; q[tail].id=i-ml; while(tail>=head && q[head].id<i-mu) head+=1; if(ans<s[i]-q[head].val) ans=s[i]-q[head].val; } printf("%d\n",-ans); } return 0; }
参考自: http://hi.baidu.com/sulipol/blog/item/734f6f50ce93392a42a75b92.html