单调栈与单调队列

单调栈与单调队列

你说我都会树链剖分了,咋还不会单调栈与单调队列啊?

单调栈

问题类型:对于一个长度为 \(n\) 的序列 \(a\),对于 \(\forall 1\le i\le n\),求 \([a_1\sim a_{i-1}]\)\([a_{i+1}\sim a_n]\) 的最靠近 \(i\) 且比 \(a_i\) 大/小的问题。

以在 \(i\) 左边的区间为例。

不过说最值问题可以用线段树、ST 表、树状数组之类的,为啥要学这个嘞?

  • 实现简单
  • 速度较快,毕竟线段树、树状数组常数都挺大的
  • 不会其他的:

那么我们来看看原理。

不妨先手搓一个样例玩玩。

假设数组为 \([1,2,3,4,5]\),求最大值。

那么我们挨个看看 \(i\)

\(i\) 数列
\(1\) \([1]\)
\(2\) \([1,2]\)
\(3\) \([1,2,3]\)
\(4\) \([1,2,3,4]\)
\(5\) \([1,2,3,4,5]\)

发现第 \(i\) 次操作有且仅有 \(a_i\) 被新加入数列。即 \([a_1\sim a_{i-1}]\) 是不变的。如果暴力枚举就会重复,效率极低。

所以我们考虑贪心的思想。

因为是求最大值,如果 \(\exist \ i<j,a_i\le a_j\) 的情况,是不是从现在开始,在 \(a_i\) 的有生之年就不可能对答案有贡献。

因为后面的区间如果包含了 \(a_i\),也必定包含了 \(a_j\),所以 \(a_i\) 一定会被 \(a_j\) 所替代,也就无法产生贡献。

那我们就不需要考虑 \(a_i\),可以把它踢出去。

所以我们存储在序列里的元素一定是一个具有单调性的序列。

我们考虑用栈进行存储。

如果 \(i<j,a_i\le a_j\),即 \(a_i\) 已成为废品,那么就让它从栈中弹出。重复执行这一步骤。

最后再加入 \(a_j\) 就算完成了一次操作。

来看看代码。

for(int i=1;i<=n;++i)
	{
		while(!st.empty()&&h[st.top()]<h[i])
		{
			ans[st.top()]=i;
			st.pop();
		}
		st.push(i);
	}

虽然看起来是双层循环,是 \(O(n^2)\),但是如果仔细算算,发现 while 里面的操作总共最多执行 \(n\),所以总时间复杂度是 \(O(n)\)

单调队列

单调栈的升级版。升级在于规定了区间长度为 \(m\)

即当前为 \(i\),与 \(i\) 有关系的区间为 \([i-m-1,i-1]\)\([i+1,i+m+1]\)。求最值。

以在 \(i\) 左边的区间为例。

那么我们再来手搓玩玩。序列还是 \([1,2,3,4,5]\),区间长度为 \(3\)

\(i\) 序列
\(1\) \([1]\)
\(2\) \([1,2]\)
\(3\) \([1,2,3]\)
\(4\) \([2,3,4]\)
\(5\) \([3,4,5]\)

其中只有 \(i\in \left\{ 3,4,5\right\}\) 时才有意义。所以只需要考虑 \(3\le i\le 5\)

可能不够直观,换种方式看看?

\[[1\ 2\ 3]\ 4\ 5\\ \ 1\ [2\ 3\ 4]\ 5\\ \ 1\ 2\ [3\ 4\ 5]\\ \]

注意到,每次改动仅是多加上一个数 \(a_{i+1}\),并减去一个数 \(a_{i-m+1}\)。所以说就是单调栈的升级版。

俗称滑动窗口。感觉十分形象。

这个是真正的区间最值。但上文说过,线段树、ST 表、树状数组都太慢且不容易实现。

老规矩,还是维护一个具有单调性双端队列,这样可以模拟从尾部进,从队头出。

对于每次“滑动”,先将滑动所加元素加入队列尾部,不断踢出不比其更优的元素。这个原理和单调栈相似。

然后由于是滑动,所以队列前面的元素可能已经超过了窗口,那我们将它踢出队列。

最后将新加元素加到队列尾部。

代码:

for(int i=1;i<=n;++i)
	{
		while(!dq.empty()&&a[dq.back()]<=a[i])
			dq.pop_back();
		while(!dq.empty()&&dq.front()<=i-k)
			dq.pop_front();
		dq.push_back(i);
		ans[i]=a[dq.front()];
	}

完结撒花。

posted @ 2025-09-02 23:34  Atserckcn  阅读(17)  评论(0)    收藏  举报