理解:
先进先出(食堂打饭,先来的先打饭,就能先从队伍里出去了)
基本知识:
9个数全部放入队列后,初始化队列如下:
head = 1;
tail = 10; (队列中已经有9个元素了,tail指向队尾的后一个位置)
这样初始化的原因是:我们这里引入了两个变量head和tail。head用来记录队列的队首(即第一位),tail用来记录队列的队尾(即最后一位)的下一个位置。为啥不用tail不直接记录队尾,却要记录队尾的下一个位呢?这是因为当队列中只剩下一个元素时,队首和队尾重合会带来一些麻烦。所以我们规定队首和队尾重合时,队列为空。
此时head(1)和tail(10)之间的数就是目前队列中“有效”的数。要想删除一个数的话,head ++就OK了,这样仍然可以保持head和tail之间的数为目前队列中“有效”的数。这样做虽然浪费了一个空间,却节省了大量的时间(算法题就是拿空间换时间),这是非常划算的。
写在前面:
队列是一种特殊的线性结构,它只允许在队列的首部(head)进行删除操作,这成为“出队”;
在队列的尾部(tail)进行插入操作,这称为“入队”;
当队列中没有元素时(即head = tail),成为“空队列”
操作:
(1)在队首删除一个数的操作:((出队)(dequeue))
head ++;
(2)在队尾增加一个数(x)的操作:((入队)(enqueue))
q[tail] = x ;
tail ++;
用处:
是学习广度优先搜索以及队列优化的Bellman-Ford最短路算法的核心数据结构。所以现在将队列的三个基本元素(一个数组,两个变量)封装为一个结构体类型,如下:
struct queue
{
int data[N]; // 队列的主体,用来存储内容
int head; // 队首
int tail; // 队尾
};
基本操作代码:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int i;
int q[102] = {0,6,3,1,7,5,8,9,2,4},head,tail;
//初始化队列
head = 1;
tail = 10; //队列中已经有9个元素了,tail指向队尾的后一个位置
while(head < tail) // 当队列不为空的时候执行循环
{
//打印队首并将队首出队
cout << q[head] << ' ';
head ++;
//先将新队首的数添加到队尾
q[tail] = q[head];
tail ++;
//再将队首出队
head ++;
}
getchar();
getchar();
return 0;
}
分类:
1.普通队列:
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
//向队尾插入一个数
q[ ++ tt] = x;
// 从队头弹出一个数
hh ++;
//队头的值
q[hh];
//判断队列是否为空
if(hh <= tt)
{
not empty
}
2.循环队列
//hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;
// 向队尾插入一个数
q[tt ++] = x;
if(tt == N) tt = 0;
// 从队头弹出一个数
hh ++;
if(hh == N) hh = 0;
//队头的值
q[hh];
//判断队列是否为空
if(hh != tt)
{
not empty
}
3.单调队列
常见模型:找出滑动窗口的最大值/最小值+
模板:
int i;
int hh = 0, tt = -1;
for(i = 0;i < n;i ++)
{
while(hh <= tt && check_out(q[hh])) hh++; // 判断队头是否滑出窗口
while(hh <= tt && check(q[tt], i)) t --;
q[ ++ tt] = i;
}
题目:
题目1:(普通队列纯裸题)
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数 x
- ;
pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。
现在要对队列进行 M个操作,其中的每个操作 3 和操作 4都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。接下来 M行,每行包含一个操作命令,操作命令为 push x
,pop
,empty
,query
中的一种。
输出格式
对于每个 empty
和 query
操作都要输出一个查询结果,每个结果占一行。
其中,empty
操作的查询结果为 YES
或 NO
,query
操作的查询结果为一个整数,表示队头元素的值。
数据范围
1≤M≤100000,1≤x≤10^9,
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
AC代码1:(数组模拟)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int q[N];
int hh = 0,tt = -1;
int main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int i;
int m;
cin >> m;
while(m --)
{
string s;
cin >>s;
if(s == "push")
{
int x;
cin >> x;
q[++ tt] = x;
}
if(s == "pop") hh ++;
if(s == "empty")
{
if(hh <= tt) cout << "NO" << endl;
else cout << "YES" << endl;
}
if(s == "query") cout << q[hh] << endl;
}
return 0;
}
AC代码2:(STL)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
queue< int >q;
int hh = 0,tt = -1;
int main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int i;
int m;
cin >> m;
while(m --)
{
string s;
cin >>s;
if(s == "push")
{
int x;
cin >> x;
q.push(x); // push函数:在队尾插入元素
}
if(s == "pop") q.pop(); // pop函数:弹出队头元素
if(s == "empty")
{
if(q.empty()) cout << "YES" << endl; // empty函数:查询队列是否为空,空返回是,不空返回否
else cout << "NO" << endl;
}
if(s == "query") cout << q.front() << endl; // front函数:查询队头元素
}
return 0;
}
题目2:(单调队列纯裸题)
题目描述:
给定一个大小为 n≤106的数组。有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。只能在窗口中看到 k个数字。每次滑动窗口向右移动一个位置。以下是一个例子:该数组为 [1 3 -1 -3 5 3 6 7]
,k为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。第一行包含两个整数 n和 k,分别代表数组长度和滑动窗口的长度。第二行有 n数,代表数组的具体数值。同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int a[N],q[N];// a数组存数字 q数组存下标
int main()
{
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(false);
int i;
int n,k;
cin >> n >> k;
for(i = 0 ; i < n ; i ++) cin >> a[i];
//输出每个区间的最小值
int hh = 0,tt = -1;
//枚举每一个元素
for(i = 0 ; i < n ; i ++)
{
//判断队头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]) hh ++; // 队列不为空且当前区间的第一个数(i是当前区间最后一个数)的下标 > 队头,说明已经出队,所以队头要向后移动一位
while(hh <= tt && a[q[tt]] >= a[i]) tt--; // 队列不为空且新插入的数a[i]小于队尾,因为我们要输出最小值,那么队尾就没有用了,要把队尾删掉q数组队尾元素出队
q[++ tt] = i; // 把数a[i]的下标存到q数组里面去,此语句在if的上面的原因是,如果数a[i]很小,我们要保证输出当前的a[i];
if(i >= k - 1) cout << a[q[hh]] << ' '; //当前数足k个才输出,不足k个就不用输出了,这是题目要求 ,而且a[q数组]是单增的,输出队头就是当前区间的最小值
}
cout << endl;
//最大值同理,但是a[q数组要单减].
int hh = 0,tt = -1; //一定要重置
//枚举每一个元素
for(i = 0 ; i < n ; i ++)
{
//判断队头是否已经滑出窗口
if(hh <= tt && i - k + 1 > q[hh]) hh ++;
while(hh <= tt && a[q[tt]] <= a[i]) tt--; // 变号
q[++ tt] = i;
if(i >= k - 1) cout << a[q[hh]] << ' ';
}
cout << endl;
return 0;
}
题目3:(单调队列+前缀和)
题目描述:
输入一个长度为n的整数序列,从中找出一段不超过m的连续子序列,使得整个序列的和最大。
例如 1,-3,5,1,-2,3
当m=4时,S=5+1-2+3=7
当m=2或m=3时,S=5+1=6
输入描述:
第一行两个数n,m(n,m≤300000)
第二行有n个数,要求在n个数找到最大子序和
输出描述:
一个数,数出他们的最大子序和
示例1:
输入:
6 4
1 -3 5 1 -2 3
输出:
7
考点:
单调队列
分析:
看到区间二字,我们首先想到前缀和数组,将原序列转化为前缀和数组后,我们要求的一段序列和,也就成了前缀和数组S[i]在[l,r]上的和,即s[r]-s[l-1];
也就是说,我们要在前缀和数组上找到两个点x,y,使得s[x]-s[y]最大,并且x-y<=m。
但又有一个问题,如果我们直接暴力枚举x,y,那必然超时,这个时候队列的作用就体现了,我们可以先固定右端点i,再去寻找一个左端点j,使得s[j]值最小,并且我们还可以确定j的范围[i-m,i-1],然后自己手捏数据找规律,我们可以发现 最优决策集合一定是一个下标位置递增,对应前缀和数组值也递增的序列。
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
int q[N],s[N];
int main()
{
int i;
int n,m;
int ans = 0;
cin >> n >> m;
for(i = 1 ; i <= n ; i ++)
{
cin >> s[i];
s[i] = s[i] + s[i - 1]; // 前缀和
}
int hh = 0,tt = 0;
for(i = 1 ; i <= n ; i ++)
{
if(hh <= tt && i - m > q[hh]) hh ++; // 区间长度大于m,出队
ans = max(ans,s[i] - s[q[hh]]); // 寻找最优解
while(hh <= tt && s[q[tt]] >= s[i]) tt --; // 若队尾不满足单调递增,出队
q[++ tt] = i; // 把i当新的决策点
}
cout << ans << endl;
return 0;
}
题目4:(队列)
题目描述:
题目描述:
有n个小组要排成一个队列,每个小组中有若干人。
当一个人来到队列时,如果队列中已经有了自己小组的成员,他就直接插队排在自己小组成员的后面,否则就站在队伍的最后面。
请你编写一个程序,模拟这种小组队列。
输入格式:
输入将包含一个或多个测试用例。
对于每个测试用例,第一行输入小组数量t。
接下来t行,每行输入一个小组描述,第一个数表示这个小组的人数,接下来的数表示这个小组的人的编号。
编号是0到999999范围内的整数。
一个小组最多可包含1000个人。
最后,命令列表如下。 有三种不同的命令:
1、ENQUEUE x - 将编号是x的人插入队列;
2、DEQUEUE - 让整个队列的第一个人出队;
3、STOP - 测试用例结束
每个命令占一行。
当输入用例t=0时,代表停止输入。
需注意:测试用例最多可包含200000(20万)个命令,因此小组队列的实现应该是高效的:
入队和出队都需要使用常数时间。
输出样例
对于每个测试用例,首先输出一行“Scenario #k”,其中k是测试用例的编号。
然后,对于每个DEQUEUE命令,输出出队的人的编号,每个编号占一行。
在每个测试用例(包括最后一个测试用例)输出完成后,输出一个空行。
数据范围
1≤t≤1000
样例1:
输入:
2
3 101 102 103
3 201 202 203
ENQUEUE 101
ENQUEUE 201
ENQUEUE 102
ENQUEUE 202
ENQUEUE 103
ENQUEUE 203
DEQUEUE
DEQUEUE
DEQUEUE
DEQUEUE
DEQUEUE
DEQUEUE
STOP
2
5 259001 259002 259003 259004 259005
6 260001 260002 260003 260004 260005 260006
ENQUEUE 259001
ENQUEUE 260001
ENQUEUE 259002
ENQUEUE 259003
ENQUEUE 259004
ENQUEUE 259005
DEQUEUE
DEQUEUE
ENQUEUE 260002
ENQUEUE 260003
DEQUEUE
DEQUEUE
DEQUEUE
DEQUEUE
STOP
0
输出:
Scenario #1
101
102
103
201
202
203
Scenario #2
259001
259002
259003
259004
259005
260001
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
queue<int> q[N],tq;
map <int,int> team;
int main()
{
int i,t;
int k=0;
while(cin >> t && t != 0)
{
cout << "Scenario #" << ++ k << endl;
while(!tq.empty()) tq.pop();
team.clear();
for(i = 0 ; i < N ; i++) while(!q[i].empty()) q[i].pop(); // //初始化
for(i = 0 ; i < t ; i ++)
{
int n,x;
cin>>n;
while(n --)
{
scanf("%d",&x);
team[x]=i;//使用map将同一组数据映射到同一个i,i代表这个数据的组别
}
}
string s;
while(cin>>s)
{
int v;
if(s=="STOP") break;
if(s=="ENQUEUE")
{
scanf("%d",&v);
int i = team[v];//查询这个元素的组别
if(q[i].empty()) tq.push(i);//如果当前组内没有元素被enqueue,就在teamqueue中插入这个组
q[i].push(v);
}
else
{
if(q[tq.front()].size()==1)
{
cout << q[tq.front()].front() << endl;
q[tq.front()].pop();
tq.pop();//如果这个组中所有插入的元素都将要被dequeue,就在teamqueue中pop这个组
}
else
{
cout << q[tq.front()].front() << endl;
q[tq.front()].pop();
}
}
}
cout << endl;
}
}