线段树维护单调栈类问题 学习笔记

先放参考资料
学长的博客1
学长的博客2
Yubai的博客
粉兔大佬的博客
首先搞明白这个东西是干什么的
有的题目里面会出现一种类似单调栈一样的模型,而这种题通常会出现不止一个或动态的单调栈,有时会让你统计信息,直接做复杂度炸天,通常需要cdq分治,吉司机线段树等神奇操作,而我们要说的方法可以在\(nlog^2\)的时间内解决,并且具有普适性

概要

普通线段树是每个节点维护一个区间的信息,\(pushup\)的时候直接合并
而这种题目中信息一般不能直接合并,因为一个区间的信息只考虑了一个区间的限制,放到全局由于限制增加可能不成立
这时我们应该引入一个函数来实现\(pushup\)中合并信息的过程,并且这个函数的复杂度不能太高
我们在线段树中保存两个域:\(data\)\(ans\),其中一个存附加信息,一个存答案
\(data\)域存储的信息的特点是:
1.一般是题目中直接给出的信息,但对答案统计具有限制作用
2.没有东西限制他,所以可以像普通线段树一样\(pushup\)
\(ans\)域存储信息的特点是:
1.多为统计的答案,但由于收到限制,所以不能直接统计
2.表示一区间在满足本区间限制时的答案,因此合并时可能发生改变
这个函数有两种实现方式,大佬的博客也写的很清楚
第一种求的是考虑本区间限制后本区间的答案
image
分析一下,如果到了叶子,那么直接判断是否满足限制就是是否有贡献
由于我们要做一个单调栈,假如现在是单调递增,看左子树满不满足限制
如果左边满足,根据单调性右边也满足,所以直接返回右边的答案(经过处理之后),再在左边递归
否则左边会全部被弹干净,直接右边递归
显然每次递归一边,那么复杂度是\(logn\)
这种写法实际上存在局限性,因为他只适合满足可加性的答案统计,不好做\(max\)之类的东西,那么就有第二个板子了
第二个求的是仅考虑当前区间一半部分的限制时,另一半的答案
image
发现差别在直接加上,这是我们定义的优越性所在
剩下的基本和普通线段树一样,但是要按顺序递归子树,按照单调栈的单调性

楼房重建

这个就是满足可加性的题,可以用第一个板子(当年的我)
就是斜率单调递增的最长序列,对每个点分别维护,放上早年的丑陋代码

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=100050;
struct node{
	int l,r,len;
	double ma;
}a[4*N];
inline void qi(int id)
{
	a[id].ma=max(a[id*2].ma,a[id*2+1].ma);	 
} 
inline int pushup(double mx,int id)
{
	if(a[id].ma<=mx)return 0;
	if(a[id].l==a[id].r)
	{
		if(a[id].ma<=mx)return 0;
		else return 1;
	}
	if(a[id*2].ma<=mx)return pushup(mx,id*2+1);
	else return pushup(mx,id*2)+a[id].len-a[id*2].len; 
}
inline void build(int id,int l,int r)
{
	a[id].l=l;a[id].r=r;
	if(l==r)return;
	int mid=(l+r)>>1;
	build(id*2,l,mid);build(id*2+1,mid+1,r);
}
inline void change(int id,int p,double v)
{
	if(a[id].l==a[id].r)
	{
		a[id].ma=v;a[id].len=1;
		return;
	}
	int mid=(a[id].l+a[id].r)>>1;
	if(p<=mid)change(id*2,p,v);
	else change(id*2+1,p,v);
	qi(id);
	a[id].len=a[id*2].len+pushup(a[id*2].ma,id*2+1);
}
signed main()
{
	int n,m;cin>>n>>m;
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int x,y;scanf("%lld%lld",&x,&y);
		double v=y/(double)x;
		change(1,x,v);
		printf("%lld\n",a[1].len); 
	 } 
	return 0;
}

陶陶摘苹果

这个是支持修改,发先修改的时候修改的是答案,所以直接修改时候\(pushup\)就行了
由于是看博客胡的所以没有码

维护dp

这种题常见的出现形式是dp,直接出的不多
首先要写一个dp方程,基本转移很显然,只是附加限制很多
思考怎么实现附加限制,大多数情况附加限制和原序列是一个映射,一种思路是翻转坐标系,这样通过区间查询就少一个限制
然后接着用上面的板子实现维护转移

God knows

就是刚才说的,这个是维护取\(max\)转移
我采用的不是一开始就把限制建好,而是每次将限制插入,这样保证转移正确,因为一个\(f\)肯定不用考虑他之后的限制
查询的时候\(v\)会不断增大,开一个全局变量保存就好了,每次查询前置-1



#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=250050;
int a[N],f[N],w[N],b[N],n;
struct tree{
	int l,r,mav,ans;
}tr[4*N];
inline int clac(int id,int v)
{	
	if(tr[id].l==tr[id].r)return (tr[id].mav>v?f[b[tr[id].l]]:1e12);
	if(tr[id*2+1].mav>v)return min(tr[id].ans,clac(id*2+1,v));
	else return clac(id*2,v);
}
inline void qi(int id)
{
	tr[id].mav=max(tr[id*2].mav,tr[id*2+1].mav);
	tr[id].ans=clac(id*2,tr[id*2+1].mav);
}
void build(int id,int l,int r)
{
	tr[id].l=l;tr[id].r=r;tr[id].ans=1e12;tr[id].mav=-1e12;
	if(l==r)return;int mid=(l+r)>>1;
	build(id*2+1,mid+1,r);build(id*2,l,mid);
	qi(id);
}	
void change(int id,int p)
{
	if(tr[id].l==tr[id].r){tr[id].mav=b[tr[id].l];return;}
	int mid=(tr[id].l+tr[id].r)>>1;
	if(p<=mid)change(id*2,p);
	else change(id*2+1,p);
	qi(id);
}
int ga;
int get(int id,int l,int r)
{
	if(l<=tr[id].l&&r>=tr[id].r)
	{	
		int an=clac(id,ga);
		ga=max(ga,tr[id].mav);
		return an;
	}	
	int mid=(tr[id].l+tr[id].r)>>1;
	if(l>mid)return get(id*2+1,l,r);
	if(r<=mid)return get(id*2,l,r);
	int an1=get(id*2+1,mid+1,r),an2=get(id*2,l,mid);
	return min(an1,an2);
}
signed main()
{	
	cin>>n;
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]),b[a[i]]=i;
	for(int i=1;i<=n;i++)scanf("%lld",&w[i]);
	a[n+1]=b[n+1]=n+1;w[n+1]=0;	
	build(1,0,n+1);
	memset(f,0x3f,sizeof(f));f[0]=0;
	for(int i=1;i<=n+1;i++)
	{
		ga=-1;int s=get(1,0,a[i]);
		f[i]=((s>=1e12)?0:s)+w[i];
		change(1,a[i]);
	}
	cout<<f[n+1]<<endl;
	return 0;
}

牛半仙的妹子序列

一样,发现是方案计数,那么只要把维护\(min\)变成和就好了,其他都一样
还有要在最开始把0的位置插进去,不然调到死。。。不放码了

总结

一个(可能)比较有用的模型,当然主要靠熟练运用

posted @ 2021-11-01 07:06  D'A'T  阅读(87)  评论(0编辑  收藏  举报