【学习笔记】单调队列 & 单调栈

单调队列和单调栈都是维护单调性的线性数据结构。

如果了解过 RMQ 的同学可能知道,大部分 RMQ 数据结构的区间查询复杂度都是 \(O(\log n)\) 级别的。但是单调队列和单调栈却能在 \(O(1)\) 的时间内完成类似操作。

想要达成这一点,要利用队列和栈的结构做文章。

达成单调性

单调队列和单调栈的单调性实际上是结构内的单调性,而且这种单调性是双重的。

第一重单调性是 值的单调性。这个很好理解,就是结构内的元素的值按照一定的顺序排列。

第二重单调性则是 过期时间的单调性,这就是单调队列和单调栈的关键所在。

每一个元素在插入结构时,都「预定」了一个过期时间。这个时间并不会改变,而且与插入时间正相关。

补充:「正相关」的意思就是「一个越……,另一个也越……」,或者说如果 \(x_i<x_j\),那么 \(y_i<y_j\)

为了维护这双重单调性,结构内必然要对元素进行删减。


不论是单调队列还是单调栈,都使用正面开口进行插入和元素调整。不同之处仅在于单调队列用后面的开口进行删除,而单调栈使用正面。接下来就使用单调队列进行演示。

首先,队列内原本保持双重单调性,但是新的元素插入,就会导致单调性被破坏:

aeALzn.png

下一步,就是将在队列前面的,不如这个元素优的元素弹出,然后再插入:

aeEz6I.png

如果一个人比你小还比你强,那么你就永远超不过 Ta 了。 —— chen_zhe

常用模型

下面介绍一些常用的单调队列/单调栈应用。

滑动窗口

顾名思义,这就是一个框定大小进行的 RMQ。

使用单调队列可以很容易解决。

限定区间大小的最大/最小区间和

这也是单调队列的经典应用。

求出前缀和以后,对于每一个区间末尾,寻找一个区间开头就是 RMQ。

单调栈内二分

这是一个比较冷门的技巧。常用于限定末尾,没有限定开头,且末尾需要插入的区间查询问题。

具体请看 【题解】最大数(咕咕咕)。

例题

滑动窗口

单调队列模板题。

#include <cstdio>
#include <cctype>
#include <queue>
using namespace std;

const int max_n = 1000000;

struct item
{
	int ind, val;
	
	item(int _i = 0, int _v = 0) : ind(_i), val(_v) { }
};

deque<item> mx, mn;

int res1[max_n], res2[max_n];

inline int read()
{
	int ch = getchar(), n = 0, t = 1;
	while (isspace(ch)) { ch = getchar(); }
	if (ch == '-') { t = -1, ch = getchar(); }
	while (isdigit(ch)) { n = n * 10 + ch - '0', ch = getchar(); }
	return n * t;
}

int main()
{
	mx.clear();
	mn.clear();
	
	int n = read(), k = read(), tmp;
	
	for (int i = 0; i < n; i++)
	{
		tmp = read();
		
		while (!mx.empty())
		{
			if (mx.front().ind <= i - k)
				mx.pop_front();
			else
				break;
		}
		while (!mn.empty())
		{
			if (mn.front().ind <= i - k)
				mn.pop_front();
			else
				break;
		}
		
		while (!mx.empty())
		{
			if (mx.back().val <= tmp)
				mx.pop_back();
			else
				break;
		}
		mx.emplace_back(i, tmp);
		while (!mn.empty())
		{
			if (mn.back().val >= tmp)
				mn.pop_back();
			else
				break;
		}
		mn.emplace_back(i, tmp);
		
		if (i >= k - 1)
		{
			res1[i-k+1] = mx.front().val;
			res2[i-k+1] = mn.front().val;
		}
	}
	
	for (int i = k; i <= n; i++)
		printf("%d ", res2[i-k]);
	putchar('\n');
	for (int i = k; i <= n; i++)
		printf("%d ", res1[i-k]);
	putchar('\n');
	
	return 0;
}

琪露诺

这就是不折不扣的用单调队列优化 DP 的模板题。

定义 \(f_i\) 为琪露诺跳到第 \(i\) 格的最大值。一步从 \(i\) 跳到 \([i+L,i+R]\),相当于 \(f_{i^{\prime}}\)\(f_{i^{\prime}-R}\)\(f_{i^{\prime}-L}\) 之间转移。

因为 \(v_i\) 是固定的,所以相当于一个 RMQ,使用单调队列解决。最后再求一个最大值就好了。

切蛋糕

这是一个相当经典的模型——限定区间大小的最大/最小区间和。这也是用单调队列解决的。

我们先求出前缀和,然后遍历。对于每一个位置 \(i\),相当于求 \(S_{i-k}\ (1\le k\le m)\) 的极值,其中 \(m\) 是大小限制。

然后,用 \(S_i-S_u\) 更新答案就可以了,\(S_u\) 是极值。注意这里的最大/最小要与区间和要求的最小/最大相反(毕竟要减掉嘛)。

Largest Rectangle in a Histogram

这是另一个经典的模型——最大折线下正方形。(咕咕咕)

猜你喜欢

转载自www.cnblogs.com/5ab-juruo/p/note-monotonous_stack_queue.html