算法自学__单调栈

参考资料:

算法简介

单调栈可以在 O ( n ) O(n) O(n) 的时间复杂度内找到序列中每个元素的下一个比它大 / 小的元素。

以找到每个元素的下一个比它大的元素(简称 NGE )为例。单调栈的基本思想是在遍历序列的过程中维护一个栈,栈中为还未找到 NGE 的元素。于是,当访问到一个元素 i 时,应该先将栈中所有比该元素小的元素出栈,并记录这些元素的 NGE 为 i ,然后再让 i 入栈。显然,栈中元素始终是单调递减的(栈底最大)。

例1 P1901 发射站

题目大意

n 个发射站,每个发射站的属性为高度发射的能量值。每个发射站发出的能量,只能被其两侧第一个比它的发射站接收到。问接受能量最多的发射站接收的能量是多少。

思路

单调栈找 NGE 模板题。

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn = 1e6+5;

int n;
int v[maxn], h[maxn];
int sum[maxn];
int pre[maxn];
int nxt[maxn];
int ans = -1;

int main(){
    
    
	cin>>n;
	for(int i=1;i<=n;i++){
    
    
		cin>>h[i]>>v[i];
	}
	stack<int> s1, s2;
	for(int i=1;i<=n;i++){
    
    
		while(!s1.empty() && h[i]>h[s1.top()]){
    
    
			sum[i] += v[s1.top()];
			s1.pop();
		}
		s1.push(i);
	}
	for(int i=n;i>=1;i--){
    
    
		while(!s2.empty() && h[i]>h[s2.top()]){
    
    
			sum[i] += v[s2.top()];
			s2.pop();
		}
		s2.push(i);
	}
	for(int i=1;i<=n;i++){
    
    
		ans = max(ans, sum[i]);
	}
	cout<<ans;
	return 0;
} 

例2 Skyscrapers (hard version)

题目大意

n 座楼,第 i 座楼限高为 m[i] ,且必须保证每座楼的左右两侧(不需要相邻)不能同时有比它高的楼,输出所有楼高度之和最大的一种方案。

思路

由题目可知,所有楼的实际高度呈先上升再下降的趋势,构成一个“单峰”函数。所以,问题的关键在于求出“峰”的位置。考虑最朴素的做法,给定一个峰的位置,可以在 O ( n ) O(n) O(n) 的时间复杂度内求出最大的高度之和,所以总的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

考虑使用单调栈优化的 dp 。定义状态 dpl[i] 表示:以楼 i 为峰,楼 i 及其左侧楼的最大高度和;定义状态 dpr[i] 表示:以楼 i 为峰,楼 i 及其右侧楼的最大高度和;定义状态 sum[i] 表示:以楼 i 为峰,所有楼的最大高度和。于是有:sum[i] = dpl[i] + dpr[i] - m[i] 。此时,问题的关键在于如何在 O ( n ) O(n) O(n) 的时间复杂度内求出所有的 dpl[i]dpr[i]

以求 dpr[i] 为例。我们先使用单调栈求出每座楼右侧第一个楼高小于等于当前楼的位置,记录在 nxt[] 中。我们从右向左遍历所有楼,假设当前访问到楼 i ,则区间 [i+1, nxt[i]-1] 的所有楼都比 i 高,所以这些楼的实际长度都为 m[i] ;而楼 nxt[i] 高度小于等于楼 i ,所以其可以取到峰值,此时,楼 nxt[i] 及其右侧的楼高之和已经保存在了 dpr[nxt[i]] 中。于是,我们得到了状态转移方程:

dpr[i] = (nxt[i]-i)*m[i] + dpr[nxt[i];

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int maxn = 5e5+5;

int n;
int m[maxn];
int pre[maxn], nxt[maxn];
int dpl[maxn], dpr[maxn];
int sum[maxn];
int M = -1;
int pos = 0;
stack<int> sl, sr;

signed main(){
    
    
	cin>>n;
	for(int i=1;i<=n;i++){
    
    
		cin>>m[i];
		// 注意初始化 nxt[] 数组
		nxt[i] = n+1;
	}
	for(int i=1;i<=n;i++){
    
    
		while(!sl.empty() && m[i]<=m[sl.top()]){
    
    
			nxt[sl.top()] = i;
			sl.pop();
		}
		sl.push(i);
	}
	for(int i=n;i>=1;i--){
    
    
		while(!sr.empty() && m[i]<=m[sr.top()]){
    
    
			pre[sr.top()] = i;
			sr.pop();
		} 
		sr.push(i);
	}
	for(int i=1;i<=n;i++){
    
    
		dpl[i] = dpl[pre[i]] + (i-pre[i])*m[i];
	}
	for(int i=n;i>=0;i--){
    
    
		dpr[i] = dpr[nxt[i]] + (nxt[i]-i)*m[i];
	}
	for(int i=1;i<=n;i++){
    
    
		sum[i] = dpl[i]+dpr[i]-m[i];
		if(sum[i] > M){
    
    
			M = sum[i];
			pos = i;
		}
	}
	for(int i=pos-1;i>=1;i--){
    
    
		m[i] = min(m[i+1], m[i]);
	}
	for(int i=pos+1;i<=n;i++){
    
    
		m[i] = min(m[i-1], m[i]);
	}
	for(int i=1;i<=n;i++){
    
    
		cout<<m[i]<<' ';
	}
	return 0;
}

例3 P1823 [COI2007] Patrik 音乐会的等待

题目大意

给定一个长度为 n 的序列 h[] ,求这个序列中有多少组下标 i, j 满足:区间 [i+1, j-1] 内的所有元素的值均小于等于 ij 中的较小者(若 i, j 相邻,也视为符合要求)。

思路

遍历序列中的每个结点,访问到结点 i 时,求出 i 之前能看到 i 的结点的数量,加到最终答案中。

回忆使用单调栈求 NGE 问题的过程,一旦确定了元素 i 的 NGE 是 j ,就意味着 i, j 满足题目中的条件。具体实现见代码。

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int maxn = 5e5+5;

struct NODE{
    
    
	int num;
	int cnt;
	NODE(int num=0, int cnt=0):num(num), cnt(cnt){
    
    }
};

int h[maxn];
int n;
stack<NODE> s;
int ans = 0;

signed main(){
    
    
	cin>>n;
	for(int i=1;i<=n;i++){
    
    
		cin>>h[i];
	}
	for(int i=1;i<=n;i++){
    
    
		int cnt = 0;
		while(!s.empty() && h[i]>=h[s.top().num]){
    
    
			// 当前元素与栈顶元素相等
			if(h[i] == h[s.top().num]){
    
    
				cnt = s.top().cnt;
			}
			ans += s.top().cnt;
			s.pop();
		}
		if(!s.empty()){
    
    
			ans++;
		}
		s.push(NODE(i, cnt+1));
	}
	cout<<ans;
	return 0;
}

例4 Discrete Centrifugal Jumps

题目大意

n n n 个高楼排成一行,每个楼有一个高度 h i h_i hi。称可以从楼 i i i 跳到 楼 j j j,当 i i i, j j j ( i < j i < j i<j )满足以下三个条件之一:

  • i + 1 = j i+1=j i+1=j

  • max ⁡ ( h i + 1 , h i + 2 , ⋯   , h j − 1 ) < min ⁡ ( h i , h j ) \max(h_{i+1},h_{i+2},\cdots,h_{j-1})<\min(h_i,h_j) max(hi+1,hi+2,,hj1)<min(hi,hj)

  • min ⁡ ( h i + 1 , h i + 2 , ⋯   , h j − 1 ) > max ⁡ ( h i , h j ) \min(h_{i+1},h_{i+2},\cdots,h_{j-1})>\max(h_i,h_j) min(hi+1,hi+2,,hj1)>max(hi,hj)

现在你在楼 1 1 1,请求出跳到楼 n n n 最少要跳几次。

2 ≤ n ≤ 3 ⋅ 1 0 5 2 \leq n \leq 3\cdot 10^5 2n3105, 1 ≤ h i ≤ 1 0 9 1\leq h_i \leq 10^9 1hi109

思路

本题和例3类似。定义状态 dp[i] 表示:移动到楼 i 所需要的最少步数。

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn = 3e5+5;

int h[maxn];
int dp[maxn];
int n;
stack<int> s1, s2;

int main(){
    
    
	cin>>n;
	memset(dp, 0x7f, sizeof(dp));
	for(int i=1;i<=n;i++){
    
    
		cin>>h[i];
	}
	dp[1] = 0;
	for(int i=1;i<=n;i++){
    
    
		while(!s1.empty() && h[i]>h[s1.top()]){
    
    
			dp[i] = min(dp[i], dp[s1.top()]+1);
			s1.pop();
		}
		if(!s1.empty()){
    
    
			dp[i] = min(dp[i], dp[s1.top()]+1);
			if(h[s1.top()] == h[i]){
    
    
				s1.pop();
			}
		} 
		s1.push(i);
		while(!s2.empty() && h[i]<h[s2.top()]){
    
    
			dp[i] = min(dp[i], dp[s2.top()]+1);
			s2.pop();
		}
		if(!s2.empty()){
    
    
			dp[i] = min(dp[i], dp[s2.top()]+1);
			if(h[s2.top()] == h[i]){
    
    
				s2.pop();
			}
		} 
		s2.push(i);
	}
	cout<<dp[n];
	return 0;
}

猜你喜欢

转载自blog.csdn.net/MaTF_/article/details/129662755