【动漫宅也要写代码 基础篇】某队列的超电磁炮

【队列】一种FIFO(先进先出)的线性表结构

萌新的个人整合,请各位大神勿喷(萌新抱腿) 【动漫宅也要写代码 基础篇】某队列的超电磁炮_第1张图片


【动漫世界观】

以下动漫背景请参照《某科学的超电磁炮(とある科学の超電磁砲)》
【动漫宅也要写代码 基础篇】某队列的超电磁炮_第2张图片
不要告诉我你作为动漫宅连炮姐都不知道(傲娇脸)


【问题背景】

学园都市一年一度(?)的超能力测试(体检)来了,为了防止发生混乱,超能力者们要在检测处排队进行超能力等级检测。

每次排在队伍最前面的人会被叫去检测,而新来的人只能排在队伍的最后。在排队过程中,不允许插队、交换位置

现在,御坂美琴(炮姐)、上条当麻(当妈)、白井黑子(百合黑子)、御坂妹(不知道是第几号)、一方通行(萝莉控)要来排队进行检测(不要问我为什么御坂妹和一方萝莉控也要来检测),请你模拟这一过程。
【动漫宅也要写代码 基础篇】某队列的超电磁炮_第3张图片


【普通队列】

这不就是排队吗?要用什么数据结构呢?

顾名思义,排队排队,就是要用队列嘛。

队列,是一种线性表结构。它的主体是一个数组,第一个队内元素所在的位置叫做队首,而最后一个元素所在的位置叫做队尾。队列允许在队首队尾进行操作。

此外,队列还具有一个特殊而重要的性质FIFO(先进先出)

什么?你说你没听懂?那么下面我们就来实际模拟一下:

  • 1.【建队与初始化】 首先我们需要一个建立一个队列
#define MaxSize 10010
int Queue[MaxSize], Head, Tail;

其中 MaxSize 队列的大小
Queue 数组存的是队内的元素,编号从 1 MaxSize1
Head 队首指针,它指向队首元素前的一项(就是说它储存队首元素的前一个位置的编号);
Tail 队尾指针,它指向队尾元素(就是说它储存队尾元素的位置的编号)。

回到问题背景。一开始,队内是没有人的,那么队头指针与队尾指针都指向0号元素(就是没有)

Head = Tail = 0;

这样就初始化好了。如下图所示:
这里写图片描述 (Head、Tail指针指向检测处(0号))

  • 2.【入队】接下来有人要入队了!

这里写图片描述【动漫宅也要写代码 基础篇】某队列的超电磁炮_第4张图片(Head指针指向检测站(0号)、Tail指针指向炮姐(1号))

那么这怎么在代码中实现呢?

void InQueue(int x) {
    Queue[++Tail] = x;
    return ;
}

这就是入队函数 x 是要入队的元素(炮姐),将Tail++给炮姐腾出位置,并将炮姐放入新的队尾

484很简单?

  • 3.【出队】又有几个人进来了,现在排在第一的炮姐要被叫去检测了

这里写图片描述【动漫宅也要写代码 基础篇】某队列的超电磁炮_第5张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第6张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第7张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第8张图片
(Head指针指向检测站(0号)、Tail指针指向一方(4号))
这里写图片描述空【动漫宅也要写代码 基础篇】某队列的超电磁炮_第9张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第10张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第11张图片
(Haed指针指向空(1号)、Tail指针指向一方(4号))

代码实现依旧简单:

int OutQueue() {
    return Queue[Head++];
}

这就是出队函数,就是把 Head 指针往后移1位,使得队首元素(炮姐)不在队中。

  • 4.【判断队伍是否为空】那么现在在进行了几轮操作(出队与入队)后,检测处的工作人员想知道这时还有没有人在排队等候,请你帮助他们。

检测处空空【动漫宅也要写代码 基础篇】某队列的超电磁炮_第12张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第13张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第14张图片
(Head指针指向空2(2号)、Tail指针指向御坂妹(5号),队列非空)

检测处空空空空空
(Head指针指向空5(5号)、Tail指针指向空5(5号),队列为空)

其实只要判断 Head 指针是否与 Tail 指针重合就好了:

bool Empty() {
    return Head == Tail;
}

5.【小结】现在知道什么是FIFO(先进先出)了吗?

因为在操作过程中,任何先入队的元素一定会先出队(因为不能插队),比如当妈就绝对不能比先来的炮姐早走(就算是CP也不能插队),所以这保证了入队与出队的有序性。

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第15张图片


【循环队列】

你以为这样事情就结束了?图样图森破,上台拿衣服。

为了模拟排队,你开了一个如下的队列:

int Queue[20001], Head, Tail;

这时来了20000个御坂妹(别问我为什么会有20000个御坂妹,就当实验还没开始吧),她们不断进行着如下操作:

1号御坂入队,然后出队;
2号御坂入队,然后出队;
3号御坂入队,然后出队;
。。。
20000号御坂妹入队,然后出队。

检测处空空空这里写图片描述空
(Head指针指向空20000(20000号)、Tail指针指向空20000(20000号))

这时,我们可爱的20001号御坂“最后之作”来了,她也想来玩玩。
可是正当她想入队的时候,竟然 SF (一般是数组越界)了!!

明明队里一个人都没有,怎么不能进入?御坂御坂不解地问道。

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第16张图片

原来,每次操作后, Head 指针与 Tail 指针都会往后移1位,当第20000号御坂出队后, Head 指针与 Tail 指针已经都指向了20000,也就是 Queue 数组的上限。这时再入队,肯定是会 SF 的。

现在令你限时解决这个问题,不然一方萝莉控将会对你进行严肃的制裁

其实这种因Head与Tail指针到达数组上界而无法入队的情况叫做假溢出。这时候要用循环队列解决。

什么是循环队列呢?就是在 Head Tail 超过 MaxSize1 后将其重新变为0。具体代码实现如下:

#define MaxSize 20001
int Queue[MaxSize], Head, Tail, Size;
void InQueue(int x) {
    Tail = (Tail + 1) % MaxSize, ++Size;
    Queue[Tail] = x;
    return ;
}
int OutQueue() {
    int Ret = Queue[Head];
    Head = (Head + 1) % MaxSize, --Size;
    return Ret;
}
bool Empty() {
    return !Size;
}

我们发现,在原有的变量的基础上,多了一个 Size 。因为头尾指针在循环后不好以是否相等判断队列长度。所以多了一个 Size 来记录队列长度

在入队与出队时,将原来的 Head++ Tail++ 改为了Head = (Head + 1) % MaxSize,Tail = (Tail + 1) % MaxSize,同时更新 Size ,这样就可以保证在 Head Tail 等于 (MaxSize1) 时,下一项是MaxSize % MaxSize=0。这样就像是把队列头尾连在了一起,所以叫循环队列

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第17张图片

这样问题就解决了。皆大欢喜!

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第18张图片

【小结】循环队列是普通队列的一种改进,用于节省储存空间,建议在不知道操作次数多少时尽量使用循环队列。


【单调队列】

虽然排队的问题圆满解决绝了,但我们的任务还未结束。

  • 【问题】超能力等级测试后,炮姐和她的小伙伴们站成了一排,请问你能求出每段长度为k的区间内等级最高的是谁吗?

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第19张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第20张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第21张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第22张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第23张图片
已知
上条当麻——Lev.0 把妹之手 幻想杀手;
一方通行——Lev.5 矢量操纵;
御坂妹妹——Lev.2 缺陷电力;
御坂美琴——Lev.5 超电磁炮;
白井黑子——Lev.4 瞬间移动。

  • 【题解】

这是一道经典的单调队列题目

顾名思义,单调队列可以保证队内元素时刻保持单调(单调递增、单调递减、单调非减、单调非增)

单调队列虽然叫“队列”,但其实和队列有很大差别:为了保证队内元素单调,会强行舍弃队中的一些元素。这点与FIFO的队列有很大不同,所以应该把单调队列看作是原数列的一个单调子序列

1.【建队与初始化】单调队列的建队与初始化与队列相同,但建议使用循环队列。在此不赘述。

2.【入队】单调队列的入队是与普通队列区别最大的一项,下以单调非增队列为例:

void InQueue(int x) {
    int To = (Tail + 1) % MaxSize, ++Len;
    while(Queue[Tail] < x && Len) {
        To = (To - 1) % MaxSize;
        --Len;
    }
    Queue[To] = x;
    return ;
}

入队时,将新入队的元素与队内的元素从后往前一一比较,直到找到第一个不比它小的元素,然后将新入队的元素插入该元素后,并将该元素后的所有原有元素舍弃。

回到原题,我们举k=2的情况。现在将1号当妈入队,因为队内为空,所以,将Tail指针指向当妈

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第24张图片
(Head = 0,Tail = 1)

因为还没有到第1个长度为2的区间[1,2],所以接着讲一方入队。这时我们的当妈就被(欢乐地)挤了出去:
【动漫宅也要写代码 基础篇】某队列的超电磁炮_第25张图片
(Head = 0, Tail = 1)

这时,到了第一个长度为2的区间[1,2],所以弹出队首(队列中最大的一项),就是该区间内最大的一项。所以区间[1,2]的答案为Lev.5。

3.【出队】出队比较简单,判断队首是否在需要操作的区间内,是就留下,不是就舍弃:

int OutQueue() {
    if(Queue[Head]在区间内) reutrn 0;
    int Ret = Queue[Head];
    Head = (Head + 1) % MaxSize, --Len;
    return Ret; 
}

再回到原题,现在要从[1,2]区间变成[2,3]区间,所以要将1号出队,3号入队。
但是我们发现队首是2号一方,2号在[2,3]区间内,所以不用出队
【动漫宅也要写代码 基础篇】某队列的超电磁炮_第26张图片
(Head = 0, Tail = 1)

将3号御坂妹入队,显然挤不走一方:
【动漫宅也要写代码 基础篇】某队列的超电磁炮_第27张图片【动漫宅也要写代码 基础篇】某队列的超电磁炮_第28张图片
(Head = 0, Tail = 2)

所以[2,3]区间内的答案还是队首一方,Lev.5。

接着将[2,3]变为[3,4],2号出队,4号入队。
我们发现队首是2号一方,不在[3,4]区间内,出队:
空【动漫宅也要写代码 基础篇】某队列的超电磁炮_第29张图片
(Head = 1, Tail = 2)

然后4号炮姐入队,将3号御坂妹挤走:
空【动漫宅也要写代码 基础篇】某队列的超电磁炮_第30张图片
(Head = 1, Tail = 2)

所以[3,4]区间内的答案就是队首炮姐,Lev.5。

剩下以此类推。

4.【小结】
单调队列虽然没有普通队列的应用广泛,使用起来也容易与重复,但还是属于常见的数据结构。
而且单调队列虽然思想简单,操作起来还是有一定的难度的。(我就被卡了好久)
所以建议大家不妨多写几道题练练手:(以下题目来自洛谷)
http://www.luogu.org/problem/lists?name=&orderitem=pid&tag=56


【PS】 C++的STL里有一个叫“优先队列”的模板,虽然叫队列但实际功能更像是另外一个数据结构——,所以在此我就不讲了,放到以后堆的内容里。(虽然单调队列还算是优先队列的一种)

想了解的朋友可以自行百度。就连队列也有STL的模板哟。

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第31张图片


OK,今天就讲到这里。撒哟那拉,米娜桑。

【动漫宅也要写代码 基础篇】某队列的超电磁炮_第32张图片

你可能感兴趣的:(【动漫宅也要写代码 基础篇】某队列的超电磁炮)