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

单调队列和单调栈是两种神奇的数据结构。此处的单调是指不断维护在队列和栈中的元素,使得其始终满足一个性质,如 (a_i>a_{i-1})。可以用较高的效率和较短的代码来解决动态区间最大(小)类的问题。

单调队列

单调队列在维护的过程中,需要对队尾的元素进行维护,如果当前的元素与即将加入的元素不满足此单调性质,就将其弹出。

如果想维护一个区间,可以采用双向队列,在队首维护不满足区间的元素。

在这里,我会以 STL 中包括在 queue 头文件的 deque 来实现双向队列。单调队列维护序列动态区间一般模板如下:

for(/*遍历序列*/)
{
    /*取得所要入队的元素*/ 
    while(!que.empty()&& /*不满足这个单调性质*/)
    	que.pop_back();   //弹出。 
    que.push_back(); //将元素压入队列
    while(/*如果不在此区间内*/) 
    	que.pop_front();  //同样需要弹出。 
}

取区间最大和最小的时间复杂度就为 (Theta(1))

cout<<que.front();

熟练运用模板而不局限于模板。单调有许多变体,将单调队列与其它的算法和数据结构结合在一起,可以实现更强大的功能。


例题:P1886 滑动窗口 /【模板】单调队列

本题是一道典型的单调队列,需要在窗口(区间)滑动的过程中,维护一个单调递增和单调递减的序列。在队列中存储数字和其编号,分别用来维护“单调”和区间。套用上面的模板,就可以解决此问题。

代码如下:

#include<bits/stdc++.h>
using namespace std;
int a[1000005],n,m,k,cnt=1;
struct node
{
    int idx,val; 
}now;
deque<node> q1;
deque<node> q2;
int main()
{
    cin>>n>>k;
    for(register int i=1;i<=n;i++)
        cin>>a[i];
    for(register int i=1;i<=n;i++)
    {
        now.idx=i; 
			now.val=a[i]; 
        while(q2.empty()==0&&a[i]<=q2.back().val)
        	q2.pop_back();
        q2.push_back(now);
        while(i-k>=q2.front().idx)
    		q2.pop_front(); 
        if(i>=k) //此处是因为本题的特殊性质,i<k 的话窗口放不下,在前 k 此不用输出。
            cout<<q2.front().val<<" ";
    }
    cout<<endl;
    for(register int i=1;i<=n;i++)
    {
        now.idx=i; 
			now.val=a[i]; 
        while(q1.empty()==0&&a[i]>=q1.back().val)
        	q1.pop_back(); 
        q1.push_back(now); 
        while(i-k>=q1.front().idx) 
        	q1.pop_front();
        if(i>=k) 
            cout<<q1.front().val<<" ";
    }
    return 0;
}

单调栈

其实单调栈和单调队列的思想是一致的,都是靠弹出元素来维护“单调”的性质。但是在实际操作中,单调队列比单调栈更常用,因为单调栈能解决的问题,一般单调队列也能解决;但单调队列能解决的问题,单调栈不一定能解决。

单调栈的模板:

for(/*遍历这个序列*/)
{
	/*取得所要入栈的元素*/ 
	while(st.empty()==0&& /*栈顶元素不满足单调性质*/)
	{
		st.pop() //栈顶元素出栈;
		//更新结果;
	}
	que.push_back(); //将元素压入栈
}

例题:P5788 【模板】单调栈

感兴趣的可以尝试做做这道题,可以分别用单调队列和单调栈打两种代码。


小结

单调队列和单调栈是帮助我们优化算法的数据结构,很多的题中离不开它们的身影。熟练运用,熟练改编,更多用法需要自己去发现。

原文地址:https://www.cnblogs.com/win10crz/p/12835723.html