单调栈/单调队列——专题

§单调栈

Usage:单调栈可以找到从左/右遍历第一个比当前元素小/大的元素的位置,以及比当前元素小/大的元素的数量
1.单调递增栈,则从栈顶到栈底的元素是严格递增的——stack.top()最小。
2.单调递减栈,则从栈顶到栈底的元素是严格递减的——stack.top()最大。
注:我们存放的一般是下标,而不是元素。但是作为比较的标准是下标对应的元素。

//模板:
for(int i=1;i<=n;i++)
{
	//单调递增栈,栈顶是最小的
	while(stk.size() && a[stk.top()] <= a[i])//根据情况改变比较符号
		stk.pop();
	//记录状态答案
	stk.push(i);
}

单调栈练习题一:洛谷-P5788 【模板】单调栈

题意:求每个元素a【i】右方(i+1 to n),第一个值比它大的元素下标。
ps:upper_bound的做法放一边,这里不做考虑。

代码一:单调栈模板写法,为了得到每个位置对应右侧的第一个下标,倒着遍历,维护单调递增栈(栈顶最小),每个位置的答案就对应此时栈顶存放的下标(栈空即为0)。

int n,m,a[maxn],ans[maxn];
stack<int> stk;
void solve()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=n;i>=1;i--)
	{
		while(stk.size() && a[stk.top()]<=a[i])
			stk.pop();
		if(stk.empty())
			ans[i] = 0;
		else
			ans[i] = stk.top();
		stk.push(i);
	}
	for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
}

代码二:伪单调栈写法:单调栈思想—向右不断延伸,数组r【i】记录的是,值不大于a【i】的元素下标最大是多少。

int n,m,k,q,a[maxn],ans[maxn];
void solve()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=n;i>=1;i--)
	{
		int st=i;
		while(st<n && a[st+1]<=a[i])
			st=ans[st+1];
		ans[i]=st;
	}
	for(int i=1;i<=n;i++)
	{
		if(ans[i]+1>n)
			cout<<0;
		else cout<<ans[i]+1;
		cout<<' ';
	}
}

单调栈练习题二:牛客-Bad Hair Day

题意:找出对于每个a【i】,从右方起,连续且小于它的元素最多有多少个,将所有个数相加为答案。

代码:和上题类似,但不需要输出每个点的答案的话,正序遍历,维护一个单调递增栈(栈顶最小),总贡献即为每次遍历,压栈前,栈的大小。

ll n,m,k,q,a[maxn];
stack<ll> stk;
void solve()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	ll ans=0;
	for(int i=1;i<=n;i++)
	{
		while(stk.size() && a[stk.top()]<=a[i])
			stk.pop();
		ans += stk.size();
		stk.push(i);
	}
	cout<<ans<<endl;
}

单调栈练习题三:洛谷-P2659 美丽的序列

题意:求整个序列的所有区间中,区间最小值×区间长度 的最大答案。

代码:正序维护单个元素不比它小最左可到哪L【i】,逆序维护单个元素不比它小最右可到哪R【i】,枚举每个元素,计算最大的a【i】× (R【i】-L【i】+1)即可。

ll n,m,k,q,a[maxn];
int l[maxn],r[maxn];
void init()
{
	for(int i=1;i<=n;i++)
	{
		int st=i;
		while(st>1 && a[st-1]>=a[i])
			st=l[st-1];
		l[i]=st;
	}
	for(int i=n;i>=1;i--)
	{
		int st=i;
		while(st<n && a[st+1]>a[i])
			st=r[st+1];
		r[i]=st;
	}
}
void solve()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	init();
	ll ans=0;
	for(int i=1;i<=n;i++)
	{
		ll val = a[i]*(r[i]-l[i]+1);
		ans = max(ans,val);
	}
	cout<<ans<<endl;
}

单调栈练习题四:牛客-区区区间间间

题意:求整个序列所有区间的最大值减最小值总和。

代码:大体和上一题一样,第一遍跑出所有区间的最大值,a【i】全取负后再跑一遍即得到了所有区间的最小值,把所有最大值减去所有最小值即可。

ll n,m,k,q,a[maxn];
int l[maxn],r[maxn];
ll getpos()
{
	for(int i=1;i<=n;i++)
	{
		int st=i;
		while(st>1 && a[st-1]<=a[i])//<=
			st=l[st-1];
		l[i]=st;
	}	
	for(int i=n;i>=1;i--)
	{
		int st=i;
		//a[i+1]向左的时候l[i+1]会计算<=的情况,所以此处单取<
		while(st<n && a[st+1]<a[i])
			st=r[st+1];
		r[i]=st;
	}
	ll ans=0;
	for(int i=1;i<=n;i++)
	{
		ans += a[i]*(r[i]-l[i]);
		ans += a[i]*(r[i]-i)*(i-l[i]);
	}
	return ans;
}
void solve()
{
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	ll max_sum = getpos();
	for(int i=1;i<=n;i++) a[i]=-a[i];
	ll min_sum = getpos();
	cout<<max_sum-(-min_sum)<<endl;
}

单调栈练习题五:洛谷-P3467 \POI2008\PLA-Postering

题意:只用矩形的海报来完全覆盖给出的若干个矩形,覆盖的海报不能超出任何一个给出的矩形,求最少使用的海报数量。

代码:需要一定的思维转换,结论是:默认情况下需要n张海报。所有矩形的宽度不需要考虑,只考虑高,然后维维护一个单调递减的栈(栈顶最大),若当前的矩形前面已经出现了,则这两张海报可以从底部连通,需要的海报数量减一。

ll n,m,k,q,a[maxn];
stack<ll> stk;
void solve()
{
	cin>>n;
	ll w,ans=n;
	for(int i=1;i<=n;i++)
	{
		cin>>w>>a[i];
		while(stk.size() && stk.top()>=a[i])
		{
			if(a[i]==stk.top()) ans--;
			stk.pop();
		}
		stk.push(a[i]);
	}
	cout<<ans<<endl;
}

单调栈练习题六:HDU1506-Largest Rectangle in a Histogram

参考博客
题意:在柱状图中寻找一个面积最大的矩形。

代码:维护一个单调递减的栈(栈顶最大),当添加的元素比栈顶小时,在不断弹出元素的过程中,因为我们单调栈存储的是下标,每个弹出元素与当前要插入的下标的差值即为矩形的宽,乘以下标对应元素的值即高即为当前元素可形成的矩形面积,更新出最大的答案。

1.在给定序列的最后插入一个值为-1的元素保证所有元素最终都会被弹出,以计算全局结果。

2.解释下其中关键的:a [ top ] = a [ i ] 这里对a进行直接修改的操作是为了优化,因维护的是单调递增栈且栈中存放的是下标,在后续计算这个新插入的元素的贡献时,top至i这一段被弹出的元素是虚拟存在的,并未完全消失,体现在我们计算矩形面积公式上: a [ top ] × ( i - top ) (建议手动模拟一下数据便于理解)

ll n,m,k,q,a[maxn];
stack<ll> stk;
void solve()
{
    while(cin>>n)
    {
        if(!n) break;
        for(int i=1;i<=n;i++) cin>>a[i];
        a[n+1]=-1;
        ll ans=0,top,val;
        for(int i=1;i<=n+1;i++)
        {
            if(stk.empty() || a[stk.top()] <= a[i])
				stk.push(i);
            else
            {
                while(stk.size() && a[stk.top()]>a[i])
                {
                    top = stk.top();
                    stk.pop();
                    val = a[top]*(i-top);
                    ans = max(ans,val);
                }
                stk.push(top);
                a[top]=a[i];//
            }
        }
        cout<<ans<<endl;
    }
}

单调栈练习题七:牛客-删删删越小越好

给出长度最大2e7的一串数,要求删去k个位置上的数字,使结果串字典序最小。
维护一个单调递减的栈(栈顶最大),即按正序遍历下来,最后入栈的元素一定比前面的都大,否则弹出栈顶并删除弹出的下标对应的数字。

bool vis[21234567];
void solve()
{
	string s;
	int k;
	cin>>s>>k;
	int pos=0,len=s.size();
	stack<int> stk;
	while(k && pos<len)
	{
		while(stk.size() && s[stk.top()]>s[pos] && k)
			stk.pop(),vis[stk.top()]=1,k--;
		stk.push(pos++);
	}
	while(k--)
	{
		vis[stk.top()]=1;
		stk.pop();
	}
	bool f=0;
	for(int i=0;i<s.size();i++)
	{
		if(!vis[i])
		{
			if(s[i]=='0' && f)
				cout<<s[i];
			else if(s[i]!='0')
				cout<<s[i],f=1;
		}
	}
	if(!f) cout<<0<<endl;
}

§单调队列

Usage:单调队列,即单调递减或单调递增的队列,一般用于求指定区间内的最值问题
可认为是带容量的优先队列,但是容量指的不是元素个数,是按新旧程度衡量

1. 队列中的元素在原来的列表中的位置是由前往后的(随着循环顺序入队)。
2. 队列中元素的大小是单调递增或递减的。

ps:跟单调栈一样,一般也是存下标比较便捷。

//模板
int L=1,R=0;
for(int i=1;i<=n;i++)
{
	//k容量的单调递减队列
	while(L<=R && Q[L]+k<=i) L++;
	while(L<=R && a[Q[R]]<=a[i]) R--;//根据情况改变比较符号
	Q[++R]=i;
	//记录状态答案
}

单调队列练习题一:洛谷-P2032 扫描

求区间最大,模板

ll n,m,k,q,a[maxn];
struct node
{
	int id,v;
}Q[maxn];
int ans[maxn];
void solve()
{
	read(n);read(k);
	for(int i=1;i<=n;i++) read(a[i]);
	int L=1,R=0;
	for(int i=1;i<=n;i++)
	{
		while(L<=R && Q[L].id+k<=i) L++;
		while(L<=R && Q[R].v<=a[i]) R--;
		Q[++R].v=a[i];
		Q[R].id=i;
		if(i>=k) ans[i]=Q[L].v;
	}
	for(int i=k;i<=n;i++)
		cout<<ans[i]<<'\n';
}

单调队列练习题二:洛谷-P1886 滑动窗口 /【模板】单调队列

求区间最小和最大,模板

ll n,m,k,q,a[maxn];
struct node
{
	int v,id;
}Q[maxn];
int L,R;
void solve()
{
	read(n);read(k);
	for(int i=1;i<=n;i++) read(a[i]);
	L=1,R=0;
	for(int i=1;i<=n;i++)
	{
		while(L<=R && Q[L].id+k<=i) L++;
		while(L<=R && Q[R].v>=a[i]) R--;
		Q[++R].v=a[i];
		Q[R].id=i;
		if(i>=k) cout<<Q[L].v<<' ';
	}
	cout<<endl;
	L=1,R=0;
	for(int i=1;i<=n;i++)
	{
		while(L<=R && Q[L].id+k<=i) L++;
		while(L<=R && Q[R].v<=a[i]) R--;
		Q[++R].v=a[i];
		Q[R].id=i;
		if(i>=k) cout<<Q[L].v<<' ';
	}
}

单调队列练习题三:洛谷-P1714 切蛋糕

题目要求是取连续的区间,区间长度最小1,最大k,使取出来的区间和最大化。
前缀和处理一下是必须的,然后维护一个单调递增(容量为k)的队列,因队列是单调递增,队列首是最小的那个,每次判断当前位置的前缀和减去队列首的前缀和是否会更大即可。

ll n,m,k,q,a[maxn];
ll sum[maxn];
deque<int> Q;
void solve()
{
	read(n);read(m);
	for(int i=1;i<=n;i++) read(a[i]);
	for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];
	
	ll ans=-INF;
	Q.push_back(0);
	for(int i=1;i<=n;i++)
	{
		while(Q.front()+m<i)
			Q.pop_front();
		while(Q.size() && sum[Q.back()]>=sum[i])
			Q.pop_back();
		ans = max(ans,sum[i]-sum[Q.front()]);
		Q.push_back(i);
	}
	cout<<ans<<endl;
}

单调队列优化DP一:洛谷-P1725 琪露诺

从i可到达【i+L,i+R】,那么对于i来说,可从【i-R,i-L】过来。
因起点为0,则下标小于L的点不可到达,枚举【L,n】的情况,更新一个单调递减队列,队列头下标比i-R还小的弹出,队列尾下标对应的dp值比dp【i-L】还小的弹出,队列头下标对应的就是【i-R,i-L】这一区间内的最大dp值,
状态转移方程为dp【i】= a【i】+ dp【Q.front()】。

注意,维护进栈的时候,需要判断进栈的这个点是否可达。

手写单调队列:

ll n,m,k,q,a[maxn];
int dp[maxn],Q[maxn];
void solve()
{
	int l,r;
	cin>>n>>l>>r;
	for(int i=0;i<=n;i++)
	{
		cin>>a[i];
		if(i&&i<l) dp[i]=-INF;
	}
	int s=1,t=0;
	int ans=-INF;
	for(int i=l;i<=n;i++)
	{
		while(s<=t && Q[s]<i-r) s++;
		while(s<=t && dp[Q[t]]<=dp[i-l]) t--;
		Q[++t]=i-l;
		dp[i]=a[i]+dp[Q[s]];
		if(i>n-r) ans=max(ans,dp[i]);
	}
	cout<<ans<<endl;
}

STL-deque实现:

ll n,m,k,q,a[maxn];
int dp[maxn],vis[maxn];
deque<int> Q;
void solve()
{
	int L,R;
	read(n);read(L);read(R);
	for(int i=0;i<=n;i++) read(a[i]);
	//i -> [i+L,i+R]
	//== [i-R,i-L] -> i
	vis[0]=1;
	int ans=-INF;
	for(int i=L;i<=n;i++)
	{
		while(Q.size() && Q.front()<i-R) Q.pop_front();
		if(vis[i-L])
		{
			while(Q.size() && dp[Q.back()]<=dp[i-L]) Q.pop_back();
			Q.push_back(i-L);
		}
		if(Q.size())
		{
			dp[i] = a[i]+dp[Q.front()];
			vis[i]=1;
			if(i>n-R) ans=max(ans,dp[i]);
		}
	}
	cout<<ans<<endl;
}

单调队列优化DP二:牛客-Tower of Hay

单调队列优化DP三:牛客-\SCOI2010\股票交易

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_45928596

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值