队列
例题1:周末舞会
问题描述
假设在周末舞会上,男士们和女士们进入舞厅时,各自排成一队。跳舞开始时,依次从男队和女队的队头上各出一人配成舞伴。规定每个舞曲只能有一对跳舞者。若两队初始人数不相同,则较长的那一队中未配对者等待下一轮舞曲。现要求写一个程序,模拟上述舞伴配对问题。
输入格式
第一行男士人数 \(m\) 和女士人数 \(n\ (m\le 1000, n\le 1000)\)
第二行舞曲的数目 \(k\ (k\le 1000)\)。
输出格式
共 \(k\) 行,每行两个数,表示配对舞伴的序号,男士在前,女士在后。
2 4
6
1 1
2 2
1 3
2 4
1 1
2 2
算法分析

男生和女生的出队号码是一个循环数列
#include <cstdio>
int main(){
int g = 0, l = 0, m, n, k;
scanf("%d%d%d", &m, &n, &k);
while (k--) {
printf("%d %d\n", ++g, ++l);
if (g == m) g = 0;
if (l == n) l = 0;
}
return 0;
}
队列的概念
队列是一种先进先出(First In First Out, FIFO)的数据结构。

- 队列的概念及基本术语
队头、队尾、入队、出队:队列(Queue)是一种运算受限的特殊线性表。队列是从表的一端(队尾rear)入队,从表的另一端(队头front)出队。
“先进先出”:假设有队列 \(Q=(a_1,a_2,a_3,\dots,a_n)\),则队列 \(Q\) 中的元素是按 \(a_1,a_2,a_3,\dots,a_n\) 的顺序依次入队,也只能按照 \(a_1,a_2,a_3,\dots,a_n\) 的顺序依次出队。因此,队列也被称作先进先出线性表(FIFO-First In First Out)。
在入队、出队的过程中,队列呈现如下几种状态:
队空:队列中没有任何元素;
队满:队列空间已全被占用;
溢出:
当队列已满,却还有元素要“入队”,就出现“上溢(overflow)”;
当队列已空:却还要做“出队”操作,就出现“下溢(underflow)”。
两种情况合在一起,统称为队列的“溢出”,也是“真溢出”。
队列的存储
队列可以用数组表示,并设置两个指针:队头指针 front 和队尾指针 rear。

为简化操作,通常约定 front 指向队头元素所在的位置,rear 就指向队尾元素的下一个位置(即 front 就指向队头元素,rear 指向队尾元素的后一个位置)。
队列的操作
- 定义:
初始化:队头、队尾指针均指向 0。
front = rear = 0;

- 入队:元素先进入 rear 的位置,然后 rear 再加 1,若
rear == N,表明队列已满。
que[rear++] = x;

- 出队:先取队首(front)元素,再出队(front 加 1),若
front == rear,表明队列已空。
x = que[front++];

再看周末舞会
算法:队列模拟
分别设置两个队列代表男队与女队,初始时男士、女士依次“入队”;
接着进行 \(k\) 次模拟:每次取各队队头元素“配对”,跳完后从队头“出队”并重新从队尾“入队”。
#include <cstdio>
const int N = 2007;
//fa,fb分别为两个队列的队头;ra,rb分别为两个队列的队尾
int qa[N], qb[N], fa, fb, ra, rb;
int main() {
int m, n, k;
scanf("%d%d%d", &m, &n, &k);
//男生女生依次入队
for (int i = 1; i <= m; ++i) qa[ra++] = i;
for (int i = 1; i <= n; ++i) qb[rb++] = i;
while (k--) {
printf("%d %d\n", qa[fa], qb[fb]);
qa[ra++] = qa[fa++], qb[rb++] = qb[fb++];
}
return 0;
}

想想:每队最多 1000 人,为什么数组要开到 \(2007\) 这么大?
- 假溢出

maxn为数组大小。
随着入队与出队的不断进行,队头指针在数组中不断向队尾方向移动,而在队首前面产生一片不能利用的“空闲区”,当队尾指针指向数组最后一个位置,即 rear == maxn-1 时,如果再有元素入队,就会出现“溢出”,这种溢出称作“假溢出”,因为空间并没有被占满。
那么,如何解决这种“假溢出”呢?
每次向“空闲区”整体移动,效率差!
让数组首尾相连,形成“环”,效率高!
循环队列

初始化(图a):
front = rear = 0;,此时队列为空;如何判断队满(图d1,d2):由于
front==rear是队空的标志,因此我们不能采用图d1的方式,把数组空间用满,而是要空出一个单元(不固定)隔在front和rear之间,当(rear+1)%maxn==front时,表明队列已满。
| 普通队列 | 循环队列 |
|---|---|
| 入队 | 入队 |
| que[rear++] = x; | que[rear++] = x; if (rear == maxn) rear = 0; |
| 出队 | 出队 |
| x = que[front++]; | x = que[front++]; if (front == maxn) front = 0; |
#include <iostream>
const int N = 1007;
struct Queue {
int q[N], fr, rr;
void push(int x) {
q[rr++] = x;
if (rr == N) rr = 0;
}
void pop() {
if (++fr == N) fr = 0;
}
int front() { return q[fr]; }
} qa, qb;
int main() {
int m, n, k;
scanf("%d%d%d", &m, &n, &k);
for (int i = 1; i <= m; ++i)
qa.push(i);
for (int i = 1; i <= n; ++i)
qb.push(i);
while (k--) {
printf("%d %d\n", qa.front(), qb.front());
qa.push(qa.front()), qb.push(qb.front());
qa.pop(), qb.pop();
}
return 0;
}
STL-queue
#include <queue>
在 STL(C++标准模板库)中,类 queue 专门实现了队列操作。

queue 中的常用操作:
front():返回 queue 中的第一个元素的引用。
back():返回 queue 中最后一个元素的引用。
push():在 queue 的尾部添加一个元素。
pop():删除 queue 中的第一个元素。
size():返回 queue 中元素的个数。
empty():如果 queue 中没有元素,则返回 true。
再看周末舞会
#include <cstdio>
#include <queue>
std::queue<int> qa, qb;
int main() {
int m, n, k;
scanf("%d%d%d", &m, &n, &k);
for (int i = 1; i <= m; ++i) qa.push(i);
for (int i = 1; i <= n; ++i) qb.push(i);
while (k--) {
printf("%d %d\n", qa.front(), qb.front());
qa.push(qa.front()), qb.push(qb.front());
qa.pop(), qb.pop();
}
return 0;
}
也可以用 struct 来完成
#include <cstdio>
const int N = 1007;
struct Queue {
int que[N], fr, re;
void push(int x) {
//如果队列已满,则无法入队
if (full()) return;
que[re++] = x;
if (re == N) re = 0;
}
void pop() {
//如果队列为空,则无法出队
if (empty()) return;
if (++fr == N) fr = 0;
}
int front() {
return que[fr];
}
bool empty() {
return fr == re;
}
bool full() {
return (re+1)%N == fr;
}
} qa, qb;
int main() {
int m, n, k;
scanf("%d%d%d", &m, &n, &k);
for (int i = 1; i <= m; ++i) qa.push(i);
for (int i = 1; i <= n; ++i) qb.push(i);
while (k--) {
printf("%d %d\n", qa.front(), qb.front());
qa.push(qa.front()), qb.push(qb.front());
qa.pop(), qb.pop();
}
return 0;
}
例题2:取牌游戏
问题描述
\(\text{Bessie}\) 正在使用一堆共 \(K\) 张(\(N\le K\le 1\times 10^5\), \(N\ |\ K\),即 \(K\) 是 \(N\) 的倍数)纸牌与 \(N-1\) 个(\(2\le N\le 100\))朋友玩取牌游戏。纸牌中共包含 \(M = {K \over N}\) 张 \(\text{good}\) 牌和 \(K - M\) 张 “\(\text{bad}\) 牌。 \(Bessie\) 负责发牌,她当然想独占所有 \(\text{good}\) 牌,因为她喜欢赢。
她的朋友怀疑她会耍诈,所以他们给出如下一些限制:
(1)游戏开始时,将最上面的牌发给 \(\text{Bessie}\) 右手边的人;
(2)每发完一张牌,她必须将接下来的 \(P\) 张牌 (\(1\le P\le 10\)) 一张一张地依次移到最后放在牌堆的底部。
(3)以逆时针方式持续给每位玩家发牌
\(\text{Bessie}\) 迫切想赢,请你帮助她算出所有 \(\text{good}\) 牌放置的位置,以便 \(\text{Bessie}\) 得到所有 \(\text{good}\) 牌。牌从上到下依次按 \(1,2,3,\dots\) 编号。

输入格式
第一行,三个用空格间隔的整数: \(N, K, P\)
输出格式
\(M\) 行,从顶部按升序依次输出 “good” 牌的位置
3 9 2
3
7
8
算法分析1. 普通队列模拟
结合条件(1)(3)可知,"\(Bessie\)" 每轮是第 \(N\) 个拿到牌。

根据数据范围,大致推导出队列存储容量“上界”为 \(K+P*K\le 1100000\)。
#include <cstdio>
#include <algorithm>
const int N = 1.1e6 + 7, M = 5e4 + 7;
//qv-队列,a-Bessie手里的牌,len-每人摸牌的数量
//fv-队列头指针,rv-队列尾指针
int qv[N], a[M], len, fv, rv;
int main() {
//n-玩游戏的人数,k-扑克牌的数量,p-每次移动牌的数量
int n, k, p;
scanf("%d%d%d", &n, &k, &p);
//扑克牌的编号入队
for (int i = 1; i <= k; ++i) qv[rv++] = i;
//一共需要发k张牌
for (int i = 1; i <= k; ++i) {
//i%n==0 时,是 Bessie 的牌
if (i%n == 0) a[++len] = qv[fv];
++fv;//发出一张牌
//依次将 p 张牌放入牌堆底部
for(int j = 1; j <= p; ++j)
qv[rv++] = qv[fv++];
}
std::sort (a+1, a+1+len);
for (int i = 1; i <= len; ++i)
printf("%d\n", a[i]);
return 0;
}
算法分析2. 循环队列模拟

队列存储容量“上界”大致设为 \(10^5\) 即可。
#include <cstdio>
#include <algorithm>
const int N = 1e5 + 7, M = 5e4 + 7;
int qv[N], a[M], len, fv, rv;
int main() {
int n, k, p;
scanf("%d%d%d", &n, &k, &p);
for (int i = 1; i <= k; ++i)
qv[rv++] = i;
for (int i = 1; i <= k; ++i) {
if (i%n == 0) a[++len] = qv[fv];
if (++fv == N) fv = 0;
for(int j = 1; j <= p; ++j) {
qv[rv++] = qv[fv++];
if (rv == N) rv = 0;
if (fv == N) fv = 0;
}
}
std::sort (a+1, a+1+len);
for (int i = 1; i <= len; ++i)
printf("%d\n", a[i]);
return 0;
}
算法分析3. STL
#include <cstdio>
#include <queue>
#include <algorithm>
std::queue<int> que;
const int M = 5e4 + 7;
int a[M], len;
int main() {
int n, k, p;
scanf("%d%d%d", &n, &k, &p);
for (int i = 1; i <= k; ++i) que.push(i);
for (int i = 1; i <= k; ++i) {
if (i%n == 0) a[++len] = que.front();
que.pop();
for(int j = 1; j <= p; ++j)
que.push(que.front()), que.pop();
}
std::sort (a+1, a+1+len);
for (int i = 1; i <= len; ++i)
printf("%d\n", a[i]);
return 0;
}
例题3:H数
算法分析1:穷举法
从 H 数的定义可以看出,如果一个数是 H 数,那么将它的 \(2,3,5,7\) 四种因子全部约去以后只剩下 1。
根据 H 数的这一特性用“穷举法”解决。程序运行后虽然输出了正确的解,但随着 \(n\) 越来越大,出解的时间变得越来越慢,尤其是当 \(n\gt 3000\) 时,更是无法满足竞赛的时间要求。
#include <cstdio>
int v[4] = {2, 3, 5, 7};
int main() {
int n, h, order = 0, t;
scanf("%d", &n);
if (n == 0) { puts("0"); return 0; }
h = 1, order = 1;
while (order < n) {
t = ++h;
for (int i = 0; i < 4; ++i)
while (t%v[i] == 0) t /= v[i];
if (t == 1) ++order;
}
printf("%d", h);
return 0;
}
算法分析2:构造法(生成法)
从因子出发由小到大地生成 H 数。假如用一个线性表来存放 H 数,称这个表为 H 数表,则 H 数表中每个元素的 2 倍数、3 倍数、5 倍数、7 倍数均是 H 数,分别用 4 个队列(不妨称为 2 倍队、3 倍队、5 倍队、7 倍队)存放。然后利用这 4 个队列生成 H 数表。




#include <iostream>
using LL = long long;
const int N = 1e4 + 7;
//四个队列构成的数组,每一行代表一个队列,队列头初始化为2,3,5,7
LL que[4][N] = {{2}, {3}, {5}, {7}};
//队列里已有一个元素,队尾全部变为 1
int fr[4], re[4] = {1, 1, 1, 1}, m[4] = {2, 3, 5, 7};
int main() {
int n;
LL mn = 1;
scanf("%d", &n);
while (--n) {
//每一次选出四个队列的最小值,作为新得出的 H 数
mn = que[0][fr[0]];
for (int i = 1; i < 4; ++i)
if (mn > que[i][fr[i]])
mn = que[i][fr[i]];
//队列头若为此次选出的 H 数,则应出队,可能有多个队列头同时出现相同的数
for (int i = 0; i < 4; ++i) {
if (que[i][fr[i]] == mn) ++fr[i];
//将此轮 H 数分别乘以 2,3,5,7 得到新的 H 数,分别加入各自队列
que[i][re[i]++] = mn * m[i];
}
}
printf("%lld", mn);
return 0;
}
算法分析3:构造法的优化
考虑每个数都一定会被分别乘以 \(2,3,5,7\),而且一定是较小的数先乘,所以我们只需分别记录当前应该乘以 \(2,3,5,7\) 的数的下标,不断向后移动即可。
分别用 4 个指针指向 H 倍数表中的元素,表示 H 数的“4 个倍数表”的队头。

#include <iostream>
using LL = long long;
const int N = 1e4 + 7;
LL ans[N] = {1}, que[4], m[4] = {2, 3, 5, 7}, len;
int main() {
int n;
LL mn = 1;
scanf("%d", &n);
while (--n) {
mn = ans[que[0]] * m[0];
for (int i = 1; i < 4; ++i)
if (mn > ans[que[i]] * m[i])
mn = ans[que[i]] * m[i];
for (int i = 0; i < 4; ++i)
if (mn == ans[que[i]] * m[i])
++que[i];
ans[++len] = mn;
}
printf("%lld", mn);
return 0;
}
例题4:超级素数
问题描述
一个素数如果从个位开始,依次去掉一位数字、两位数字、三位数字……直到只剩下一个数字,中间所有剩下的数都是素数,则该素数为一个超级素数。
例如:2333 是一个超级素数,因为 2333,233,23,2 都是素数。
请写一个程序,给定一个整数 \(x\),求大小不超过 \(x\) 的所有超级素数。
输入格式
一行,给出一个整数 \(x\ (1\le x\le 10^9)\)
输出格式
第一行,一个整数 \(k\),表示 \(x\) 以内超级素数的个数。接下来一行 \(k\) 个整数,输出所有 \(x\) 以内的超级素数,这些数按从小到大的顺序排列。
100
13
2 3 5 7 23 29 31 37 53 59 71 73 79
Hint
将 2,3,5,7 依次入队,接着每次从队头出队的元素,尝试在其后面添加一个数字使其成为素数,如果可以,将新产生的素数入队,依此类推。
#include <iostream>
const int N = 107;
int que[N] = {2, 3, 5, 7}, fr, rr = 4;
bool isPrime(int x) {
if (x<2 || x>2&&x%2==0) return false;
for (int i = 3; i*i <= x; i += 2)
if (x%i == 0) return false;
return true;
}
int main() {
int x;
scanf("%d", &x);
if (x < 7) {
if (x >= 5) printf("3\n2 3 5");
else if (x >= 3) printf("2\n2 3");
else if (x >= 2) printf("1\n2");
else puts("0");
return 0;
}
//当产生一个新的不大于x的元素时,将其入队
//总有队列为空的时候,即后面再产生的元素都大于x
while (fr < rr) {
//从队头出队一个元素,此时元素还存在数组中,只是fr后移了一个位置。
int u = que[fr++];
for (int i = 1; i <= 9; i += 2) {
int v = 10*u + i;
if (v<=x && isPrime(v)) que[rr++] = v;
}
}
printf("%d\n", rr);
for (int i = 0; i < rr; ++i)
printf("%d ", que[i]);
return 0;
}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
int n, t, m, x;
int temp_nation[N];
int ans;
struct node{
int s, t;
};
queue<node>ship;
node h;
int main(){
cin >> n;
for(int i = 1;i <= n;i++){
cin >> t >> m;
//将超时的船上的乘客出队
while(ship.size()){
h = ship.front();
if(h.t+86400 > t) break;
temp_nation[h.s]--;
//如果改乘客是其国家的最后一个出队,国家数--
if(temp_nation[h.s] == 0) ans--;
ship.pop();
}
for(int j = 1;j <= m;++j){
cin >> x;
h.s = x, h.t = t;
ship.push(h);
temp_nation[x]++;
if(temp_nation[x] == 1) ans++;
}
cout << ans << endl;
}
return 0;
}
//手工队列
#include <iostream>
const int N = 1e5 + 7;
int nat[N];
struct Pass {
int na, tm;
} que[N*3];
int main() {
int n, ff = 0, rr = 0, ans = 0;
scanf("%d", &n);
while (n--) {
int t, m;
scanf("%d%d", &t, &m);
while (ff < rr) {
Pass p = que[ff];
if (p.tm+86400 > t) break;
if (--nat[p.na] == 0) --ans;
++ff;
}
for (int i = 1, x; i <= m; ++i) {
scanf("%d", &x);
que[rr++] = {x, t};
if (++nat[x] == 1) ++ans;
}
printf("%d\n", ans);
}
return 0;
}

浙公网安备 33010602011771号