队列中的数据存取方式是“先进先出”,只能像队尾插入数据,从队头移出数据。队列和栈的主要问题是查找较慢,需要从头到尾一个个的查找。在某些情况下可以用优先队列,让优先级最高(最大的数或者最小的数)先出队列。
竞赛中一般用STL queue或手写静态数组实现队列。本文只介绍STL queue。
队列的主要操作如下
#include<queue>//如果要使用队列的话 必须包含这个头文件
//主要操作如下:
queue<Type>q;//定义队列,Type为数据类型,如int,float,char等
q.push(item);//把item放进队列
q.front();//返回队首元素,但不会删除
q.pop();//删除队首元素
q.back();//返回队首元素
q.size();//返回元素个数
q.empty(); //检查队列是否为空
下面给出一道例题,用STL queue 实现:https://www.luogu.com.cn/problem/P1540
#include<iostream>
#include<queue>
using namespace std;
int Hash[1103]={0};
queue<int> mc;//队列模拟内存
int main()
{
int m,n;
cin>>m>>n;
int ans=0;
while(n--)
{
int a; cin>>a;
if(!Hash[a])//如果内存中没有这个单词
{
ans++;
mc.push(a);
Hash[a]=1;
while(mc.size()>m)//内存满了
{
Hash[mc.front()]=0;
mc.pop();
}
}
}
cout<<ans<<endl;
return 0;
}
下面来接受双端队列和单调队列
双端队列是一种具有队列和栈性质的数据结构,它能在俩高端进行插入和删除,而且也只能在两端插入和删除。双端队列的用法如下:
#include<deque>//使用时必须包含这个头文件
deque<Type>dq//建立双端队列
dq[i]//返回下标为i的元素
dq.front();//返回队首元素
dq.back();//返回队尾元素
dq.pop_back();//弹出队尾元素
dq.pop_front();//弹出队头元素
dq.push_back();//在队尾插入一个元素
dq.push_front(); //在队首插入一个元素
双端队列的经典应用是单调队列。单调队列中的元素是单调有序的,且元素在队列中的顺序和原来在序列中的顺序是一致的。单调队列的复杂度是O(n)
单调队列与滑动窗口 :https://www.luogu.com.cn/problem/P1886
思路:本体暴力法的话代码很好写:从头到尾扫描,每次检查k个数字,一共检查O(nk)次。暴力法一定会超时。下面用单调队列求解,它的复杂度是O(n)
在本题中,单调队列有一下特征。
(1)队头的元素始终是队列中最小的。需要根据输出队头,但是不一定弹出。
(2)元素只能从队尾进入队列。从队尾和队头都可以弹出。
(3)序列中的每个元素都是必须进入队列。例如,x进入队尾时,和原队尾y比较,如果x<=y,就从队尾弹出y;一直弹出队尾所有比x大的元素,最后x进入队尾。这个入队操作保证了队头元素是队列中最小的。
#include<iostream>
#include<deque>
using namespace std;
const int N=1000005;
int a[N];
deque<int>q;//队列中的数据实际上是元素在原序列中的位置
int main()
{
int n,m; cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)//输出最小值
{
while(!q.empty()&&a[q.back()]>a[i]) q.pop_back();//去尾
q.push_back(i);
if(i>=m)//每个窗口输出一次
{
while(!q.empty()&&q.front()<=i-m) q.pop_front();//删头
cout<<a[q.front()]<<" ";
}
}
cout<<endl;
while(!q.empty()) q.pop_front();
for(int i=1;i<=n;i++)
{
while(!q.empty()&&a[q.back()]<a[i]) q.pop_back();
q.push_back(i);
if(i>=m)
{
while(!q.empty()&&q.front()<=i-m) q.pop_front();
cout<<a[q.front()]<<" ";
}
}
cout<<endl;
return 0;
}
单调队列与最大子序和问题
什么是子序和?给定长度为n的整数序列A,它的“子序列”定义为A中非空的一段连续匀速。例如,序列(1,2,3,4,5),前4个元素的子序和为10.
最大子序和问题按照子序有无长度限制分为两种。
问题(1):不限制子序列长度。在所有可能的子序列中找到一个子序列,该子序列最大
问题(2):限制子序列的长度,给定一个限制长度为m,找出一段长度不超过m的连续子序列,使它的子序列最大。
问题一的求解:
方法一:贪心法。逐个扫描序列中的元素并累加。加一个正数时,子序和会增加;加一个负数时,子序和会减小。如果当前得到的和变成了负数,这个负数会在接下来的累加中会减小后面的求和,所以抛弃它,从下一位置求和。
#include<bits/stdc++.h>
using namespace std;
const int INF=0x7fffffff;
int main()
{
int t; cin>>t;
for(int i=1;i<=t;i++)
{
int n;cin>>n;
int maxsum=-INF;
int start=1,end=1,p=1;
int sum=0;
for(int j=1;j<=n;j++)
{
int a; cin>>a;
sum+=a;
if(sum>maxsum){maxsum=sum;start=p;end=j;}
if(sum<0){
sum=0;
p=j+1;
}
}
printf("Case %d:\n",i);printf("%d %d %d\n",maxsum,start,end);
if(i!=t) cout<<endl;
}
return 0;
}
方法二:动态规划。定义状态dp[i],表示以a[i]结尾的最大自诩和。dp[i]的计算有两种情况:
1.dp[i]之包括一个元素,就是a[i];
2.dp[i]包括多个元素,从前面某个a[v]开始,v<i,到a[i]结束,即dp[i-1]+a[i]。去两者最大值
#include<bits/stdc++.h>
using namespace std;
int dp[10005];
int main()
{
int t; cin>>t;
for(int k=1;k<=t;k++)
{
int n;cin>>n;
for(int i=1;i<=n;i++)cin>>dp[i];
int start=1,end=1,p=1;
int maxsum=dp[1];
for(int i=2;i<=n;i++)
{
if(dp[i-1]+dp[i]>=dp[i])
dp[i]=dp[i-1]+dp[i];
else
p=i;
if(dp[i>maxsum])
{
maxsum=dp[i];start=p;end=i;
}
}
printf("Case %d:\n",k);printf("%d %d %d\n",maxsum,start,end);
if(k!=t) cout<<endl;
}
return 0;
}
问题(2)的思路:
首先求前缀和s[i],s[I]是a[1]到a[i]的和。之后问题转化为找出两个位置i,k,使s[i]-s[k]最大,并且I-k<=m.如果暴力的去检查的话复杂度是O(mn),会超时。我们不妨用一个长度为m的滑动窗口即单调队列来维护。代码如下
#include<bits/stdc++.h>
using namespace std;
deque<int>dq;
int s[100005];
int main()
{
int n,m; scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&s[i]);
for(int i=1;i<=n;i++) s[i]=s[i-1]+s[i];
int ans=-1e8;
dq.push_back(0);
for(int i=1;i<=n;i++)
{
while(!dq.empty()&&dq.front()<i-m) dq.pop_front();
if(dq.empty()) ans=max(ans,s[i]);
else ans=max(ans,s[i]-s[dq.front()]);
while(!dq.empty()&&s[dq.back()]>=s[i]) dq.pop_back();
dq.push_back(i);
}
printf("%d",ans);
return 0;
}
优先队列会结合堆来讲。