队列相关算法实现详解

以下面的例题为例,介绍队列的相关实现。

题目

假设内存中有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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

互联网的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值