单调队列 & 单调栈

单调队列

单调队列顾名思义,就是单调递增/递减的队列。

单调队列主要解决的问题是:对于一个序列,用 \(O(n)\) 时间复杂度求出以 \(1∼n−k+1\) 为开头,长度为 \(k\) 的区间最值。

我们为了维护一个具有单调性的队列,每向其中增加一个元素,我们需要拿队尾元素和新增元素进行比较,如果维护的是单调递减队列,那我们就需要把队列末尾比新增元素更小的元素踢出队列(注意区分严格单调和不严格单调),单调递增队列反之。

我们发现我们需要从队列末尾删除元素,这时我们就需要用到双端队列了。不管你是手写还是使用 std::deque 都可以实现单调队列,但 STL 多么简洁好用啊,所以作者在这篇文章中所使用的全都是 STL,尽管很多人说手写更快

所以我们得到了单调队列的模板

	ll a[(ll)1e6+86];
	deque<ll> que;
	ll n, k;
	cin >> n >> k;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
		//我们以单调递增为例
		while (!que.empty() && que.back() >= a[i]) que.pop_back();
		que.push_back(a[i]);
	}

B3667 求区间所有后缀最大值的位置

来看板题。我们发现这个要处理的问题较刚才介绍的更为复杂,模板代码竟然过不了板题?!

我们发现这个题要求处理的不仅仅是整个序列的最值了,而是限制了区间范围,这就是我们常说的滑动窗口思想。

所以如果我们还想用单调队列处理,不难发现,在单调队列里的元素是具有时效性的,对于过期的元素(就是不在要求区间范围内的元素)我们应该把它删去,那如何判断是否要删去?我们应该记录每个元素的下标,并与区间范围进行比较,超出范围就踢掉。

那我们为什么不直接维护下标呢???

所以我们的得到了真正的优先队列模板:

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

ull a[(ll)1e6+5];
deque<ull> que;
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	ll n, k;
	cin >> n >> k;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
		// 清除过期数据
		if (!que.empty() && i - que.front() > k - 1) que.pop_front();
		// 严格单调递减队列
		while (!que.empty() && a[que.back()] <= a[i]) que.pop_back();
		que.push_back(i);
		if (i >= k) cout << que.size() << endl;
	}

	return ~~ (0 ^ 0);
}

在普遍情况下,这种优先队列能够处理的问题更广,代码较之前也不是很繁琐,因此我们一般均使用这个版本。

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

当然我们还是过不了板题的,因为板题要求同时处理最大值和最小值,那我们思考一下,对于一个单调递增队列,是不是他的队首就是区间最小值?这是肯定的。那他的队尾以此类推……

当然不是最大值啦。我们要注意,具有单一单调性的单调队列一次只能维护一种区间最值,即单调递增队列维护区间最小值,单调递减队列维护区间最大值。

那这道题应该怎么办呢?很简单,一个不行,来两个就行了。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

ll a[(ll)1e6+86];
deque<ll> que1, que2;  //一个维护最小值一个维护最大值
queue<ll> minn, maxn;

int main() {
	ll n, k;
	cin >> n >> k;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
		//清除过期元素
		if (!que1.empty() && i - que1.front() > k - 1) que1.pop_front();
		if (!que2.empty() && i - que2.front() > k - 1) que2.pop_front();
		//单调递增
		while (!que1.empty() && a[que1.back()] >= a[i]) que1.pop_back();
		que1.push_back(i);
		//单调递减
		while (!que2.empty() && a[que2.back()] <= a[i]) que2.pop_back();
		que2.push_back(i);
		//更新答案,最大值最小值就是两个队列的队首元素。
		if (i >= k) {
			minn.push(que1.front());
			maxn.push(que2.front());
		}
	}
	//output
	while (!minn.empty()) {
		cout << a[minn.front()] << " ";
		minn.pop();
	}
	cout << endl;
	while (!maxn.empty()) {
		cout << a[maxn.front()] << " ";
		maxn.pop();
	}
	return ~~ (0 ^ 0);
}

单调栈

其实单调栈就是单调队列。

在许多情况下,解决实际问题中,很多题是不需要用滑动窗口的思想的,而双端队列的 que.push_back() 太难敲了,所以很多题,我们更习惯使用单调栈,你可以理解为单调队列的简化版。当然,本质区别就是单调栈是个栈,而单调队列是个队列,与单调队列相比,其只在一端进行进出。

类比单调队列我们可以得到单调栈的模板(这个是真的):

	ull a[(ll)1e6+86];
	stack<ull> st;
	ll n;
	cin >> n;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
		// 严格单调递减栈
		while (!st.empty() && a[st.top()] <= a[i]) 	st.pop();
		st.push(i);
		cout << sum << endl;
	}

B3666 求数列所有后缀最大值的位置

板题就很是简单了,思考可以发现,在栈中的元素就是要求的元素。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

ull a[(ll)1e6+86];
stack<ull> st;
ull sum ; // 维护栈中元素异或和

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);

	ll n;
	cin >> n;
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
		// 严格单调递减栈
		while (!st.empty() && a[st.top()] <= a[i]) {
			sum ^= st.top();
			st.pop();
		}
		// 此时栈中的元素异或和就是答案
		sum ^= i;
		st.push(i);
		cout << sum << endl;
	}

	return ~~ (0 ^ 0);
}

例题

P2422 良好的感觉

依照题面要求分析,不难想到,在以某个元素为最小值的区间内,因为 \(A_i\) 恒为正,最小值一定,所以区间越大,区间和越大,最后得到的结果也就越大。

所以我们尝试以每个元素作为最小值,然后扩展其最大区间,就可以得到这个最小值下的最大答案,再综合比较每个答案的大小,就可以找到最大答案了。

这道题我们介绍一种方法,同单调性单调栈双向遍历法(名字好长),这样我们可以一次得到所有元素的对应最小值区间。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

ll n;
ll a[(ll)1e5+5], sum[(ll)1e5+5];
stack<ll> st1, st2;
ll maxn;
ll l[(ll)1e5+5], r[(ll)1e5+5];

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;

	// 计算左边界
	st1.push(0); //哨兵位置,避免RE
	for (ll i = 1; i <= n; i++) {
		cin >> a[i];
		sum[i] = sum[i - 1] + a[i];
		// 单调递增栈(正向)
		while (!st1.empty() && a[st1.top()] >= a[i]) st1.pop();
		l[i] = st1.top() + 1;
		st1.push(i);
	}

	st2.push(n + 1);
	for (ll i = n; i >= 1 ; i--) {
		// 单调递增栈(反向)
		while (!st2.empty() && a[st2.top()] >= a[i]) st2.pop();
		r[i] = st2.top() - 1;
		st2.push(i);
	}
	for (ll i = 1; i <= n; i++) {

		//cout << l[i] << " " << r[i] << endl;
		// 分别尝试以每个值都作为区间最小值
		// 然后扩展满足这一最小值区间范围
		maxn = max(maxn, (sum[r[i]] - sum[l[i] - 1]) *  a[i]);
	}
	cout << maxn;

	return ~~ (0 ^ 0);
}

P4147 玉蟾宫

这应该是最经典的题目了,是用单调栈去解决最大有效子矩形问题。

我们考虑按每行去划分,然后处理出来一个高度数组 \(pos\) 存储每行可延伸最大高度,就是对该行及以上的部分做最大矩阵处理,记录每行向上可延伸的最大距离。

处理方法就是:

  • 读到一个 F\(pos_{i-1 , j} + 1 \rightarrow pos_{i,j}\)
  • 读到一个 R\(0 \rightarrow pos_{i,j}\)(因为该处不可进行向上伸展)

然后处理完成之后,我们对每行高度数组计算最大矩形面积,循环遍历每行,并且进行单调栈操作,注意这里的单调栈需要传入一个二元组,其 first 代表高度,其 second 代表长度。

中间需要记录矩形面积并进行比较,具体的处理方法我们可以直接看代码了,注释写的很详细。

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ll long long
#define dbug(x) (void)(cerr << #x << " = " << x << endl)

const int N = 1e3 + 86;
ll pos[N][N]; // 每行的高度矩阵
ll ans;

int main() {
	ll n, m;
	cin >> n >> m;
	for (ll i = 1; i <= n; i++) {
		for (ll j = 1; j <= m; j++) {
			char temp;
			cin >> temp;
			if (temp == 'F') {
				pos[i][j] = pos[i - 1][j] + 1;
			} else {
				pos[i][j] = 0;
			}
		}
	}
	for (ll i = 1; i <= n; i++) {
		ll maxn = 0; // 记录当前行的最大面积
		// 对每行进行单调栈处理,存入first高度,second(连续)长度
		stack<pair<ll, ll> > st;
		for (ll j = 1; j <= m; j++) {
			ll temp = 0; // 临时记录弹出的总长度
			// 维护单调递增栈
			while (!st.empty() && st.top().first >= pos[i][j]) {
				//如果有一个元素破坏了单调性,那么就说明栈内元素无法再继续扩大了。
				//此时我们累加当前栈内的元素,计算面积更新最大值。
				temp += st.top().second; //长度
				maxn = max(maxn, st.top().first * temp);  //当前栈顶元素为高度
				st.pop();
			}
			// 入栈,高度为当前高度,长度为累加长度+1(当前列本身)
			// 之前弹出栈的元素高度比当前元素高,也肯定可以和本元素构成矩形
			// 所以长度是temp + 1
			st.push(make_pair(pos[i][j], temp + 1));
		}
		// 如果此时栈里还有元素,说明他们高度足够低,可以一直延申。
		ll temp = 0;
		while (!st.empty()) {
			temp += st.top().second; //长度
			maxn = max(maxn, st.top().first * temp);  //当前栈顶元素为高度
			st.pop();
		}
		ans = max(ans, maxn);
	}
	cout << ans * 3;

	return ~~ (0 ^ 0);
}
posted @ 2025-10-23 22:04  Justskr  阅读(86)  评论(0)    收藏  举报