以下面的例题为例,介绍队列的相关实现。
题目
假设内存中有M个单元,每单元能存放一个单词和译义。每当软件将一个新单词存入内存前,如果当前内存中已存入的单词数不超过M−1,软件会将新单词存入一个未使用的内存单元;若内存中已存入M个单词,软件会清空最早进入内存的那个单词,腾出单元来,存放新单词。
假设一篇英语文章的长度为N个单词。给定这篇待译文章,翻译软件需要去外存查找多少次词典?假设在翻译开始前,内存中没有任何单词。
STL队列实现
#include<bits/stdc++.h>
using namespace std;
int Hash[1003]={0};
queue<int> mem;
int main(){
int m,n;
cin>>m>>n;
int cnt=0;
while(n--){
int en;
cin>>en;
if(!Hash[en]){
++cnt;
mem.push(en);
Hash[en]=1;
while(mem.size()>m){
Hash[mem.front()]=0;
mem.pop();
}
}
}
cout<<cnt<<endl;
return 0;
}
手写静态分配的循环队列
#include<bits/stdc++.h>
#define N 1003
using namespace std;
int Hash[N]={0};
struct myqueue{
int data[N];
int head,rear;
bool init(){
head=rear=0;
return true;
}
int size(){
return (rear-head+N)%N;
}
bool empty(){
if(size()==0){
return true;
}
else{
return false;
}
}
bool push(int e){
if((rear+1)%N==head){
return false;
}
data[rear]=e;
rear=(rear+1)%N;
return true;
}
bool pop(int &e){
if(head==rear){
return false;
}
e=data[head];
head=(head+1)%N;
return true;
}
int front(){
return data[head];
}
}Q;
int main(){
Q.init();
int m,n;
cin>>m>>n;
int cnt=0;
while(n--){
int en;
cin>>en;
if(!Hash[en]){
++cnt;
Q.push(en);
Hash[en]=1;
while(Q.size()>m){
int tmp;
Q.pop(tmp);
Hash[tmp]=0;
}
}
}
cout<<cnt;
return 0;
}
手写动态分配的循环队列
#include<bits/stdc++.h>
using namespace std;
#define N 1003
int Hash[N] = {0};
struct myqueue {
int *data;
int head, rear;
bool init() {
data = (int *)malloc(N * sizeof(int));
if (!data) {
return false;
}
return true;
}
int size() {
return (rear - head + N) % N;
}
bool empty() {
return size() == 0;
}
bool push(int e) {
if ((rear + 1) % N == head) {
return false;
}
data[rear] = e;
rear = (rear + 1) % N;
return true;
}
bool pop(int &e) {
if (head == rear) {
return false;
}
e = data[head];
head = (head + 1) % N;
return true;
}
int front() {
return data[head];
}
} Q;
int main() {
Q.init();
int m, n;
cin>>m>>n;
int cnt = 0;
while(n--) {
int en;
cin>>en;
if (!Hash[en]) {
++cnt;
Q.push(en);
Hash[en] = 1;
while (Q.size() > m) {
int tmp;
Q.pop(tmp);
Hash[tmp] = 0;
}
}
}
cout << cnt;
return 0;
}
双端队列和单调队列
双端队列STL的实现deque
dq[i] //返回队列中下标为i的元素
dq.front()//返回队头
dq.back()//返回队尾
dq.pop_back()//删除队尾,不返回值
dq.pop_back()//删除队头,不返回值
dq.push_back(e)//在队尾添加一个元素e
dq.push_front(e)//在队头添加一个元素e
单调队列与滑动窗口
题目
有一个长为n的序列a,以及一个大小为k的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如,对于序列 [1,3,−1,−3,5,3,6,7]以及 k=3,有如下过程:
输入格式
输入一共有两行,第一行有两个正整数n,k。 第二行n个整数,表示序列a
输出格式
输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值
解题代码
#include<bits/stdc++.h>
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;
}
单调队列与最大子序列和问题
最大子序列和问题按子序列有无长度限制分为两种。
问题(1)不限制子序列的长度。在所有可能的子序列中找到一个子序列,该子序列和最大
问题(2)限制子序列的长度。给定一个限制长度m,找出一段长度不超过m的连续子序列,使它的子序列和最大
问题(1)比较简单,用贪心法或动态规划算法,复杂度都为O(n)
问题(2)用单调队列,复杂度也为O(n)
问题(1)的求解
用贪心法或动态规划,在O(n)时间内求解。
题目
题解一 贪心法
#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;
}
题解二 动态规划
#include<bits/stdc++.h>
using namespace std;
int dp[100005];
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;
}
}
}
问题(2)的求解
可以使用单调队列的窗口、删头、去尾解决。
首先求前缀和s[i],s[i]是a[1]~a[i]的和,计算所有的s[i]~s[n],时间复杂度为O(n)。
问题(2)转换为:找出两个位置i、k,使s[i]-s[k]最大,i-k<=m
首先思考用dp求解,把问题进一步转换为:首先固定一个i,找到它左边的一个端点k,i-k<=m,使s[i]-s[k]最大,定义这个最大值是dp[i],逐步扩大i,求得所有的dp[i]。其中的最大值就是问题的解。如果简单地暴力检查,对每个i检查比它小的m个s[k],那么总时间复杂度为O(mn),将超时。
暴力检查的方法不可行,改用一个大小为m的窗口寻找最大子序列和ans。从头到尾依次把s[]的元素放入这个窗口。
(1)首先把s[1]放入窗口,并且记录ans的初始值为s[1]。
(2)接着把s[2]放入窗口(假设窗口长度大于2),有两种情况:如果s[1]<=s[2],那么更新ans=max{s[1],s[2],s[2]-s[1]};如果s[1]>s[2],那么保持ans=s[1]不变,然后从窗口中抛弃s[1],只留下s[2],因为后面再把新的s[i']放入窗口时,s[i']-s[2]比s[i']-s[1]更大。
继续这个过程,直到所有的s[]处理结束。
总结上面的思路,把新的s[i]放入窗口时:
(1)把窗口内比s[i]大的所有s[j]都抛弃,i-j<=m,因为这些s[j]在处理s[i]后面的s[i']时用不到了,s[i']-s[i]要优于s[i']-s[j],保留s[i]就可以了。
(2)若窗口内的最小的是s[k],此时肯定有s[k]<s[i],检查s[i]-s[k]是否为当前的最大子序列和,如果是,就更新最大子序列和ans;
(2)每个s[i]都会进入队列。
此时最优策略是一个“位置递增、前缀和也递增”的序列,用单调队列最合适了。s[i]进入队尾时,如果原队尾比s[i]大,则去尾;如果队头超过窗口范围m,则去头,而最小的那个s[k]就是队头。
在这个单调队列中,每个s[i]只进出队列一次,计算复杂度为O(n)
使用此方法解决上述问题的最大和的代码如下:
#include<bits/stdc++.h>
using namespace std;
deque<int>dp;
int s[100005];
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>s[i];
}
for(int i=1;i<=n;i++){
s[i]=s[i]+s[i-1];
}
int ans=-1e8;
dp.push_back(0);
for(int i=1;i<=n;i++){
while(!dp.empty()&&dp.front()<i-m){
dp.pop_front();
}
if(dp.empty()){
ans=max(ans,s[i]);
}
else{
ans=max(ans,s[i]-s[dp.front()]);
}
while(!dp.empty()&&s[dp.back()]>=s[i]){
dp.pop_back();
}
dp.push_back(i);
}
cout<<ans<<endl;
return 0;
}