先放参考资料
学长的博客1
学长的博客2
Yubai的博客
粉兔大佬的博客
首先搞明白这个东西是干什么的
有的题目里面会出现一种类似单调栈一样的模型,而这种题通常会出现不止一个或动态的单调栈,有时会让你统计信息,直接做复杂度炸天,通常需要cdq分治,吉司机线段树等神奇操作,而我们要说的方法可以在\(nlog^2\)的时间内解决,并且具有普适性
概要
普通线段树是每个节点维护一个区间的信息,\(pushup\)的时候直接合并
而这种题目中信息一般不能直接合并,因为一个区间的信息只考虑了一个区间的限制,放到全局由于限制增加可能不成立
这时我们应该引入一个函数来实现\(pushup\)中合并信息的过程,并且这个函数的复杂度不能太高
我们在线段树中保存两个域:\(data\)和\(ans\),其中一个存附加信息,一个存答案
\(data\)域存储的信息的特点是:
1.一般是题目中直接给出的信息,但对答案统计具有限制作用
2.没有东西限制他,所以可以像普通线段树一样\(pushup\)
\(ans\)域存储信息的特点是:
1.多为统计的答案,但由于收到限制,所以不能直接统计
2.表示一区间在满足本区间限制时的答案,因此合并时可能发生改变
这个函数有两种实现方式,大佬的博客也写的很清楚
第一种求的是考虑本区间限制后本区间的答案
分析一下,如果到了叶子,那么直接判断是否满足限制就是是否有贡献
由于我们要做一个单调栈,假如现在是单调递增,看左子树满不满足限制
如果左边满足,根据单调性右边也满足,所以直接返回右边的答案(经过处理之后),再在左边递归
否则左边会全部被弹干净,直接右边递归
显然每次递归一边,那么复杂度是\(logn\)的
这种写法实际上存在局限性,因为他只适合满足可加性的答案统计,不好做\(max\)之类的东西,那么就有第二个板子了
第二个求的是仅考虑当前区间一半部分的限制时,另一半的答案
发现差别在直接加上,这是我们定义的优越性所在
剩下的基本和普通线段树一样,但是要按顺序递归子树,按照单调栈的单调性
楼房重建
这个就是满足可加性的题,可以用第一个板子(当年的我)
就是斜率单调递增的最长序列,对每个点分别维护,放上早年的丑陋代码
#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的位置插进去,不然调到死。。。不放码了
总结
一个(可能)比较有用的模型,当然主要靠熟练运用