单调栈


经典用法


考虑一个问题:对于一个没有重复数字的序列,求出每个数字右侧第一个比它小的数的下标。很容易可以想到每个数字不断向右遍历,但是复杂度为 $O(n^2)$,有没有更好的办法?

使用栈来维护(下文下标从 $1$ 开始,且栈中的都是下标),不妨考虑序列 $[2,5,6,7,3,4,1,8]$。首先栈为空,压入下标 $1$,然后发现值 $5,6,7$ 都比 $2$ 大且递增,压入下标,此时栈的状况为 $(1,2,3,4$。

再来到下标 $5$,值为 $3$,比栈顶下标 $4$ 代表的值 $7$ 小,此时直接结算 $a[4]$ 的第一个比它小的数就是 $a[5]=3$,弹出 $4$,此时栈为 $(1,2,3$。不难发现,$a[3]$ 仍大于 $a[5]$,继续记录答案,然后弹出,最后发现 $a[1]<a[5]$,不弹出了,将 $5$ 压入栈,得到 $(1,5$。

来到下标 $6$,$a[6]>a[5]$,直接压入,得到 $(1,5,6$。来到下标 $7$,$a[7]$ 比 $a[1],a[5],a[6]$ 都小,那么全部记录答案,压入 $7$,得到 $(7$。来到下标 $8$,$a[8]>a[7]$,压入得到 $(7,8$,此时已经没有后续的数。

最后,弹出 $8,7$,因为没有数”主动“将它们弹出,所以右侧没有比它们小的数。最后,得到右侧第一个比序列中数小的下标为:$[7,5,5,5,7,7,0,0]$($0$ 表示不存在)

实际上,我们还可以找到对于每个数,左侧第一个比它小的数。在某个数出栈时,这个数下面的数就是左侧第一个比它小的数。比如,上述 $4$ 出栈时,下面是 $3$,故第一个比他小的数就是 $a[3]=6$,也可以得到下面的下标序列:$[0,1,2,3,1,5,0,7]$。

总而言之,对于一个数,谁让它出栈,谁就是右边第一个比它小的数(最后主动出栈意味着不存在);谁压在它下面,谁就是左边第一个比它小的数(若其在栈底意味着不存在)。

进阶用法及实现


对于含有重复数字的序列,则需要特殊方法处理。

// 这里使用了 1 开始的下标
stack<int> stk;
int a[N], ans[N][2]; // 第二维为 0,是左侧;第二维为 1,是右侧
void compute() {
	int idx; // 用于记录弹出的下标
	for (int i = 1; i <= n; i++) {
		while (stk.size() && a[stk.top()] >= a[i]) {
		// 等于栈顶也要弹出,为了保持该单调栈严格大压小的性质
			idx = stk.top();
			stk.pop();
			ans[idx][0] = stk.size() ? stk.top() : 0; // 底下的就是左边第一个比它小的
			ans[idx][1] = i; // 现在将它弹出的就是右边第一个比它小的
		}
		stk.push(i);
	}
	while (stk.size()) {
		idx = stk.top();
		stk.pop();
		ans[idx][0] = stk.size() ? stk.top() : 0; // 底下的就是左边第一个比它小的
		ans[idx][1] = 0; // 自行弹出,说明右边没有比它小的
	}
	// 由于单调栈内严格遵守大压小的性质,故 ans[i][0] 的答案都是正确的,
	// 至于 ans[i][1] 的答案,需要修正右边第一个比它小的数仍为它本身的情况
	for (int i = n - 1; i >= 1; i--) { // 第 n 个必定为 -1,不必修正
		if (ans[i][1] != 0 && a[ans[i][1]] == a[i]) { // 检查是否仍为它本身
			ans[i][1] = ans[ans[i][1]][1];
			// 例如 a[9]=3,单调栈运行过程中为了保证严格大压小,相等也要弹出,
			// 不妨设 a[11]=3 弹出了 9,那么 ans[9][1]=11,
			// 实际上是不对的。但是,ans[11][1] = 1 是正确的,那就把 11 的答案传递给 9,
			// 这样就正确了。并且,从右向左修正也保证了正确性。
			
			// 注:如果不进行修正,则某个数会向左找到第一个严格小的数,向右找到第一个
			// 小于或等于的数,在一些单调栈有关的题目具有优势
		}
	}
}

把 ≥ 改为 ≤ 就可以寻找第一个大于的数。

需要注意的是,如果不在意左边,而只在意右边第一个,则相等就不用弹出。

应用


柱状图中最大矩形