0
点赞
收藏
分享

微信扫一扫

单调栈算法

妖妖妈 2022-01-30 阅读 32
算法

单调栈

一、栈

下文均使用名为 st ,栈底为 st[1] ,栈顶为 st[top] 的数组模拟栈。

这样做的好处是,操作方便:

操作代码
查询栈的大小top
查询栈是否为空top
插入元素 x x xst[++top] = x
弹出栈顶元素top--

二、单调栈

何为单调栈

严格?

严格不等式:只含有记号 > > > < < < 的不等式称为严格不等式。

非严格不等式:含有记号 ≤ \leq ≥ \geq 的不等式称非严格不等式。

所以「严格大于」就是 > > >,「严格小于」就是 < < <

严格单调递增(减),指的是除第一个元素外,每个元素都严格大于(小于)前一个元素。

应用

考虑这样一个问题:给定一个数列,对于每个数,求出这个数之前第一个严格大于它的数。

假设该数列为: [ 81 , 45 , 11 , 0 , 14 , 26 ] [81, 45, 11, 0, 14, 26] [81,45,11,0,14,26],依次将元素插入栈。

如图 1-1,此时已插入前 4 4 4 个元素,下一个待插入元素为 14 14 14

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3C933a8d-1643421866793)(https://cdn.jsdelivr.net/gh/cjwen6/cdn/img/monotonous-stack-before.svg)]

图 1-1(图自 OI-Wiki)

为了找到 14 14 14 之前第一个比它大的元素,要将比 14 14 14 小的元素弹出,于是弹出 0 0 0 11 11 11

如图 1-2,现在栈中 14 14 14 的前一个元素为 45 45 45,所以 14 14 14 之前第一个大于它的元素是 45 45 45

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6RYHFv9-1643421866794)(https://cdn.jsdelivr.net/gh/cjwen6/cdn/img/monotonous-stack-after.svg)]

图 1-2(图自 OI-Wiki)

为什么可以这么做?

这就是「单调栈」的核心部分了。

假设下一个插入的元素为 x x x

如果 14 ≤ x 14 \leq x 14x,那么那么 14 14 14 肯定不是答案,那么比 14 14 14 还小的 0 , 11 0, 11 0,11 就更不可能是答案。

如果 14 > x 14 > x 14>x,那么 14 14 14 就是答案,不用再考虑 0 , 11 0, 11 0,11

所以, 0 , 11 0, 11 0,11 不会再对后面的答案产生贡献(影响),可以弹出。

因此,维护一个严格单调递减的栈的算法是正确的。

代码实现

#include<iostream>
#include<cstdio>

using namespace std;

int main(){
	
	int n = 6;
	int a[6] = {81, 45, 11, 0, 14, 26}, ans[6] = { };
	int st[6] = { }, top = 0;
	
	for(int i = 0; i < n; i++){
		while(top && st[top] <= a[i]){ // 注意要用 top 判栈是否为空,不为空才能判断
			printf("pop %d.\n", st[top]);
			top--;
		}
		
		if(top){
			ans[i] = st[top];
		}else{
			ans[i] = -1; // 前面没有比这个数大的,就输出 -1
		}
		
		printf("push %d.\n", a[i]);
		st[++top] = a[i];
		
		printf("stack: ");
		for(int j = 1; j <= top; j++){
			printf("%d ", st[j]);
		}
		puts("\n");
		
	}
	
	printf("ans: ");
	for(int i = 0; i < n; i++){
		printf("%d ", ans[i]);
	}
	
	return 0;
}

运行结果:

push 81.
stack: 81 

push 45.
stack: 81 45 

push 11.
stack: 81 45 11 

push 0.
stack: 81 45 11 0 

pop 0.
pop 11.
push 14.
stack: 81 45 14 

pop 14.
push 26.
stack: 81 45 26 

ans: -1 81 45 11 45 45 

时空复杂度

空间复杂度显然为 O ( n ) O(n) O(n)

观察代码核心部分:

// ……
for(int i = 0; i < n; i++){
	while(top && st[top] <= a[i]){
		printf("pop %d.\n", st[top]);
		top--;
	}
	// ……
}
// ……

两重循环,看此 O ( n 2 ) O(n^2) O(n2)

注意里层的 while 循环,是用于弹出栈顶小于待插入元素的元素。

每个元素只入栈一次,也只会出栈一次,所以里层 while 循环总共只会执行 n n n 次,整个单调栈代码的时间复杂度为 O ( n ) O(n) O(n)

三、单调栈简单应用

单调栈模板题

题目链接

FYMS-OI #32. 「模板」单调栈

Lougu P5788 【模板】单调栈

题意概括

给定一个长度为 N N N 的数列,对于每个数,求出这个数之后第一个严格大于它的元素的下标

分析

发现正着做单调栈无法处理,考虑反着做。

此时就需要维护一个严格单调递减的栈,即插入元素前把栈顶小于它的元素全部弹出。

插入前如果栈为空,则答案为 0 0 0,否则为栈顶元素。

这题的一个要注意的点在于,要求的是元素的下标,所以栈要存储下标,而非数据。

题目使用的下标为 1 ∼ n 1 \sim n 1n,方便起见程序的下标也用 1 ∼ n 1 \sim n 1n

参考代码

#include<iostream>
#include<cstdio>

using namespace std;

const int N = 3e6 + 10;

int n, a[N], ans[N];
int st[N], top;

int main(){
	
	scanf("%d", &n);
	
	for(int i = 1; i <= n; i++){
		scanf("%d", &a[i]);
	}
	
	for(int i = n; i > 0; i--){
		while(top && a[st[top]] <= a[i]){
			top--;
		}
		if(top){
			ans[i] = st[top];
		}else{
			ans[i] = 0;
		}
		st[++top] = i;
	}
	
	for(int i = 1; i <= n; i++){
		printf("%d ", ans[i]);
	}
	
	return 0;
}

Bad Hair Day

题目链接

FYMS-OI #2046. Bad Hair Day

POJ 3250 Bad Hair Day

Luogu P2866 [USACO06NOV]Bad Hair Day S

题意概括

N ( N ≤ 80 , 000 ) N (N \leq 80,000) N(N80,000) 头奶牛排成一队,每只奶牛有一个身高 h i h_{i} hi,求每只奶牛可以看到前方多少只奶牛的头发的和。

分析

奶牛 A 看到了奶牛 B 的头发,就相当于奶牛 B 的头发被奶牛 A 看到了。

求每只奶牛可以看见后面多少个奶牛的头发比较困难,但可以求每只奶牛的头发可以被前面多少只奶牛看见,这两个求法最终的和是一样的。

于是问题就被转化为「每只奶牛的头发可以被前面多少只奶牛看见」。

使用单调栈,维护一个严格单调递减的栈。

参考代码

#include<iostream>
#include<cstdio>

using namespace std;

const int N = 8e4 + 10;

int n, x;
int st[N], top;
long long ans;

int main(){
	
	scanf("%d", &n);
	
	for(int i = 0; i < n; i++){
		scanf("%d", &x);
		while(top && st[top] <= x){
			top--;
		}
		ans += top;
		st[++top] = x;
	}
	
	printf("%lld\n", ans);
	
	return 0;
}

四、单调栈离线处理 RMQ 问题

「模板」ST 表

其实是 RMQ 模板题。

何为 RMQ

RMQ 是英文 Range Maximum(Minimum) Query 的缩写,意思是查询区间最大(最小)值。

常见算法:

算法是否在线预处理时间复杂度单次询问时间复杂度总时间复杂度空间复杂度
ST 表在线 O ( n log ⁡ n ) O(n \log n) O(nlogn) O ( 1 ) O(1) O(1) O ( n log ⁡ n + n ) ≈ O ( n log ⁡ n ) O(n \log n + n) \approx O(n \log n) O(nlogn+n)O(nlogn) O ( n log ⁡ n ) O(n \log n) O(nlogn)
线段树在线 O ( n ) O(n) O(n) O ( l o g n ) O(log n) O(logn) O ( n + m log ⁡ n ) ≈ O ( m log ⁡ n ) O(n + m \log n) \approx O(m \log n) O(n+mlogn)O(mlogn) O ( n ) O(n) O(n)
………………………………
单调栈离线 O ( m log ⁡ m ) O(m \log m) O(mlogm) O ( log ⁡ n ) O(\log n) O(logn) O ( m log ⁡ m + m log ⁡ n ) ≈ O ( m log ⁡ m ) O(m \log m + m \log n) \approx O(m \log m) O(mlogm+mlogn)O(mlogm) O ( n ) O(n) O(n)

其中 n n n 为数组大小, m m m 为询问次数。

单调栈并不是 RMQ 问题的最优解。

详细内容可以前往 RMQ - OI Wiki 查看。

在线与离线

给定一个区间,如果某种算法可以即使即时回答每一个询问,则称该算法为在线的,否则称为离线的。

题目链接

Luogu P3865 【模板】ST 表

题意概括

给定 n n n 个数和 m m m 组询问,每组询问有一个区间 l , r l, r l,r,求该区间最大值。

分析

读入所有询问,然后按询问的右端点从小到大排序。

依次处理元素,维护严格单调递减栈。

如果处理到某个询问的右端点,则二分单调栈,找出栈底开始第一个下标大于此次询问的 l l l 的元素,记录答案。

详见代码注释。

参考代码

#include<iostream>
#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1e5 + 10, M = 2e6 + 10;

struct qt{
	int id, l, r;
}q[M]; // 结构体存储询问

int n, a[N], m;
int st[N], top;
int ans[M];

bool cmp(qt x, qt y){
	return x.r < y.r;
} // 按右端点从小到大排序

int main(){
	
	scanf("%d%d", &n, &m);
	
	for(int i = 1; i <= n; i++){
		scanf("%d", &a[i]);
	}
	
	for(int i = 0; i < m; i++){
		q[i].id = i;
		scanf("%d%d", &q[i].l, &q[i].r);
	}
	
	sort(q, q+m, cmp);
	
	int nw = 0; // 当前处理的询问
	
	for(int i = 1; i <= n && nw < m; i++){ // 依次处理每一个元素,插入栈中,维护严格单调递减栈
		
		while(top && a[st[top]] <= a[i]){
			top--;
		}
		st[++top] = i;
		
		while(i == q[nw].r){ // 如果当前处理到某一个询问的右端点(因为右端点可能重复,所以用 while 而非 if)
			
			int l = 1, r = top; // 二分栈中第一个下标大于此次询问的 l 的元素
			while(l < r){
				int mid = (l + r) / 2;
				if(st[mid] >= q[nw].l){
					r = mid;
				}else{
					l = mid + 1;
				}
			}
			ans[q[nw].id] = a[st[l]]; // 记录答案
			
			nw++; // 处理下一个问题
		}
	}
	
	for(int i = 0; i < m; i++){
		printf("%d\n", ans[i]);
	}
	
	return 0;
}
举报

相关推荐

0 条评论