以下动漫背景请参照《某科学的超电磁炮(とある科学の超電磁砲)》
不要告诉我你作为动漫宅连炮姐都不知道(傲娇脸)
学园都市一年一度(?)的超能力测试(体检)来了,为了防止发生混乱,超能力者们要在检测处排队进行超能力等级检测。
每次排在队伍最前面的人会被叫去检测,而新来的人只能排在队伍的最后。在排队过程中,不允许插队、交换位置。
现在,御坂美琴(炮姐)、上条当麻(当妈)、白井黑子(百合黑子)、御坂妹(不知道是第几号)、一方通行(萝莉控)要来排队进行检测(不要问我为什么御坂妹和一方萝莉控也要来检测),请你模拟这一过程。
这不就是排队吗?要用什么数据结构呢?
顾名思义,排队排队,就是要用队列嘛。
队列,是一种线性表结构。它的主体是一个数组,第一个队内元素所在的位置叫做队首,而最后一个元素所在的位置叫做队尾。队列允许在队首队尾进行操作。
此外,队列还具有一个特殊而重要的性质FIFO(先进先出)。
什么?你说你没听懂?那么下面我们就来实际模拟一下:
#define MaxSize 10010
int Queue[MaxSize], Head, Tail;
其中 MaxSize 是队列的大小;
Queue 数组存的是队内的元素,编号从 1 到 MaxSize−1 ;
Head 是队首指针,它指向队首元素前的一项(就是说它储存队首元素的前一个位置的编号);
Tail 是队尾指针,它指向队尾元素(就是说它储存队尾元素的位置的编号)。
回到问题背景。一开始,队内是没有人的,那么队头指针与队尾指针都指向0号元素(就是没有)
Head = Tail = 0;
这样就初始化好了。如下图所示:
(Head、Tail指针指向检测处(0号))
(Head指针指向检测站(0号)、Tail指针指向炮姐(1号))
那么这怎么在代码中实现呢?
void InQueue(int x) {
Queue[++Tail] = x;
return ;
}
这就是入队函数, x 是要入队的元素(炮姐),将Tail++给炮姐腾出位置,并将炮姐放入新的队尾
484很简单?
(Head指针指向检测站(0号)、Tail指针指向一方(4号))
(Haed指针指向空(1号)、Tail指针指向一方(4号))
代码实现依旧简单:
int OutQueue() {
return Queue[Head++];
}
这就是出队函数,就是把 Head 指针往后移1位,使得队首元素(炮姐)不在队中。
(Head指针指向空2(2号)、Tail指针指向御坂妹(5号),队列非空)
(Head指针指向空5(5号)、Tail指针指向空5(5号),队列为空)
其实只要判断 Head 指针是否与 Tail 指针重合就好了:
bool Empty() {
return Head == Tail;
}
5.【小结】现在知道什么是FIFO(先进先出)了吗?
因为在操作过程中,任何先入队的元素一定会先出队(因为不能插队),比如当妈就绝对不能比先来的炮姐早走(就算是CP也不能插队),所以这保证了入队与出队的有序性。
你以为这样事情就结束了?图样图森破,上台拿衣服。
为了模拟排队,你开了一个如下的队列:
int Queue[20001], Head, Tail;
这时来了20000个御坂妹(别问我为什么会有20000个御坂妹,就当实验还没开始吧),她们不断进行着如下操作:
1号御坂入队,然后出队;
2号御坂入队,然后出队;
3号御坂入队,然后出队;
。。。
20000号御坂妹入队,然后出队。
(Head指针指向空20000(20000号)、Tail指针指向空20000(20000号))
这时,我们可爱的20001号御坂“最后之作”来了,她也想来玩玩。
可是正当她想入队的时候,竟然 SF (一般是数组越界)了!!
明明队里一个人都没有,怎么不能进入?御坂御坂不解地问道。
原来,每次操作后, Head 指针与 Tail 指针都会往后移1位,当第20000号御坂出队后, Head 指针与 Tail 指针已经都指向了20000,也就是 Queue 数组的上限。这时再入队,肯定是会 SF 的。
现在令你限时解决这个问题,不然一方萝莉控将会对你进行严肃的制裁。
其实这种因Head与Tail指针到达数组上界而无法入队的情况叫做假溢出。这时候要用循环队列解决。
什么是循环队列呢?就是在 Head 或 Tail 超过 (MaxSize−1) 后将其重新变为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 等于 (MaxSize−1) 时,下一项是MaxSize % MaxSize=0。这样就像是把队列头尾连在了一起,所以叫循环队列。
这样问题就解决了。皆大欢喜!
【小结】循环队列是普通队列的一种改进,用于节省储存空间,建议在不知道操作次数多少时尽量使用循环队列。
虽然排队的问题圆满解决绝了,但我们的任务还未结束。
已知:
上条当麻——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指针指向当妈:
因为还没有到第1个长度为2的区间[1,2],所以接着讲一方入队。这时我们的当妈就被(欢乐地)挤了出去:
(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]区间内,所以不用出队。
(Head = 0, Tail = 1)
将3号御坂妹入队,显然挤不走一方:
(Head = 0, Tail = 2)
所以[2,3]区间内的答案还是队首一方,Lev.5。
接着将[2,3]变为[3,4],2号出队,4号入队。
我们发现队首是2号一方,不在[3,4]区间内,出队:
(Head = 1, Tail = 2)
然后4号炮姐入队,将3号御坂妹挤走:
(Head = 1, Tail = 2)
所以[3,4]区间内的答案就是队首炮姐,Lev.5。
剩下以此类推。
4.【小结】
单调队列虽然没有普通队列的应用广泛,使用起来也容易与堆重复,但还是属于常见的数据结构。
而且单调队列虽然思想简单,操作起来还是有一定的难度的。(我就被卡了好久)
所以建议大家不妨多写几道题练练手:(以下题目来自洛谷)
http://www.luogu.org/problem/lists?name=&orderitem=pid&tag=56
【PS】 C++的STL里有一个叫“优先队列”的模板,虽然叫队列但实际功能更像是另外一个数据结构——堆,所以在此我就不讲了,放到以后堆的内容里。(虽然单调队列还算是优先队列的一种)
想了解的朋友可以自行百度。就连队列也有STL的模板哟。
OK,今天就讲到这里。撒哟那拉,米娜桑。