简单来说,单调队列是用来解决这样的问题的:
实现一个目标容器buffer,支持3种操作:
不断地向buffer里读入元素、
时不时会去掉当前buffer里的最老的元素
不定期地询问当前buffer里的最小元素。
实现一个buffer,使得上面的操作的代价尽可能小。
而单调队列,是实现该buffer的一个好方法。
为每个读入到buffer的元素封装一个专门的结构体:
struct Element {
int time; // 这是该元素被读入buffer时的时间戳,一般从0开始
int val; // 该元素的值
};
单调队列,是一个队列,按照从队头到队尾的方向,并且满足2个性质:
(a)元素的time字段依次增加,也就是说,元素是从旧到新的
(b)元素的val字段依次增加,也就是说,元素是从小到大的
我们先考虑一下用普通的队列是怎么实现目标buffer的。
假设Q是一个普通的双向队列,支持push_back, pop_back, push_front, pop_front
然后我们不断地往Q的尾巴push元素。那么:
要插入一个元素的时候,就push_back那个元素。时间为O(1)
要删除最老元素的时候,只需要pop_front。时间为O(1)
要询问当前最小值的时候,就遍历一次队列里的所有元素。时间为O(n)
考虑一下如何改进。
当我们往Q的尾巴插入一个元素x的时候,有一件事我们是可以马上确定的:
插入x前,对于任意y属于Q,如果y.val >= x.val,那么,对于插入x之后的任意一个询问,问题的答案,都绝对不会是y,因为x既比y小,又比y要新。
也就是说,每当我们插入一个最新元素x到Q的尾巴的时候,那些比x大的元素都没有生存价值,因为他们永远不会成为之后的询问的答案,他们存在于这个世界上唯一可以做的事情就是等待某个时候被删除。
既然这样,当我们插入元素x的时候,我们可以从尾巴往头部方向扫描,把那些大于x元素都先pop出来,反正他们永远都不会成为询问的答案。最后,再插入x到尾部。这样子,性质(a)就得到维护了。
当我们需要删除所有在某个时间t之前插入的一些老元素时,只需要从队头往尾巴方向扫描,把插入时间在t之前的元素都pop出来就行了。