队列

例题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)的数据结构。

  1. 队列的概念及基本术语

队头、队尾、入队、出队:队列(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 时,如果再有元素入队,就会出现“溢出”,这种溢出称作“假溢出”,因为空间并没有被占满。

那么,如何解决这种“假溢出”呢?

  1. 每次向“空闲区”整体移动,效率差!

  2. 让数组首尾相连,形成“环”,效率高!

循环队列

  1. 初始化(图a):front = rear = 0;,此时队列为空;

  2. 如何判断队满(图d1,d2):由于 front==rear 是队空的标志,因此我们不能采用图d1的方式,把数组空间用满,而是要空出一个单元(不固定)隔在 frontrear 之间,当 (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;
}

例题5:海港

#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;
}
posted @ 2024-06-09 18:20  飞花阁  阅读(141)  评论(0)    收藏  举报
//雪花飘落效果