听上去有丶厉害,实际也很巧妙
学习了这两篇:ReMoon - 单调栈的应用 --- 笛卡尔树与虚树
ACM算法日常 - 算法合集 | 神奇的笛卡尔树 - HDU 1506
~ 简介 ~
虽然名字中带有“树”,但是笛卡尔树其实是对于一个序列的转化,并通过这个转化获得更多此序列的信息
对于一个简单的序列:$2,8,5,7,1,4$,我们可以建立如下的笛卡尔树($pos$表示原序列中的位置,$val$表示该位置的值)
笛卡尔树有这样的基本性质:
对于树上的任意一点$x$和左右儿子$left,right$,有:
1. $pos[left]<pos[x]<pos[right]$
2. $val[x]<val[left],val[right]$
即一般讲解所说的$pos$满足二叉查找树,$val$满足堆
直观点说,就是这两条延伸性质:
以树上任意一点$x$为根构成的子树中,
1. 各节点的$pos$是连续的,且对$pos$的先序遍历即为原序列顺序(由$pos$满足二叉查找树可得)
2. $x$点的$val$为全子树最小(由$val$满足堆可得)
~ 建树 ~
有了对笛卡尔树结构的了解,现在考虑怎么建立这棵树
【方法一】优先满足$val$
要想优先满足$val$的条件,那就必须从顶向下建树了
利用上面的延伸性质2,每次选取当前区间$[l,r]$中$val$的最小值所在的$pos$(记$pos=i$)作为子树的根节点
然后对于$[l,i-1],[i+1,r]$递归地不断重复上述过程
其中选取区间$val$最小值所在的$pos$可以使用线段树优化
总复杂度$O(nlogn)$
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=100005; const int INF=1<<30; int n; int val[N]; int sz; int t[N<<2]; inline void Add(int i) { int k=i+sz-1; t[k]=i; k>>=1; while(k) { int left=t[k<<1],right=t[k<<1|1]; t[k]=(val[left]<val[right]?left:right); k>>=1; } } inline int Query(int k,int l,int r,int a,int b) { if(a>r || b<l) return 0; if(a>=l && b<=r) return t[k]; int mid=(a+b)>>1; int left=Query(k<<1,l,r,a,mid),right=Query(k<<1|1,l,r,mid+1,b); return (val[left]<val[right]?left:right); } void Init() { sz=1; while(sz<n) sz<<=1; val[0]=INF; for(int i=1;i<(sz<<1);i++) t[i]=0; for(int i=1;i<=n;i++) Add(i); } int ls[N],rs[N]; inline int Build(int l,int r) { if(l>r) return 0; int pos=Query(1,l,r,1,sz); ls[pos]=Build(l,pos-1); rs[pos]=Build(pos+1,r); return pos; } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&val[i]); Init(); int root=Build(1,n); /* for(int i=1;i<=n;i++) printf("i=%d: ls=%d rs=%d\n",i,ls[i],rs[i]);*/ return 0; }
【方法二】优先满足$pos$
由于对于子树的先序遍历是原序列顺序,所以考虑按$i=1\text{ ~ }n$的顺序依次加入节点并调整树的结构,使得当前的树为子序列$[1,i]$所构成的笛卡尔树
由于$pos$满足二叉排序树,而$i$在区间$[1,i]$中$pos$最大,所以$i$插入的位置为 序列$[1,i-1]$所构成的笛卡尔树的根节点 一直向右儿子走、直到走到了空节点
这样插入后,$i$的$pos$已经满足要求了,但是$val$却不一定满足堆
于是考虑怎么调整当前的树
若$i$的$val$不满足要求,即存在某(些)祖先$j$,使得$j$为根的子树中$val$全大于$val[i]$;显然我们需要通过调整$i$的位置,使得$i$成为$j$的祖先
是这样操作的:
0. 刚刚插入完成后,可能树是这样的
1. 将$i$向上一层移动;这时由于$pos[k]<pos[i]$,所以$k$成为$i$的左儿子,$k'$依然是$k$的左儿子
2. 继续将$i$向上一层移动,相似的,$j$也应当属于$i$的左子树;不妨让$j$为$i$的左儿子,$k$为$j$的右儿子(使用这种调整方法,$j,k,k'$相互间与原来的连边相同)
以上的调整操作都是在$[1,i-1]$序列构成的笛卡尔树的最右链(即从根节点一直向右儿子走的这条路径)上进行的
在处理完后,我们对比一下调整前后的树结构,发现只有很少的地方出现了变化:
1. $k$的右儿子变成了空节点
2. $j$的父亲变成了$i$,且$j$是$i$的左儿子
3. $i$继承了原来$j$的父亲
事实上,即使$i$到$j$的路径很长很长,一共也只有这三个地方发生了变化,所以我们的调整不是很复杂
现在最大的问题变成,如何找到$j$
目光回到最右链上,由于$val$满足堆,于是最右链上的各节点$val$是单调递增的;可以考虑用单调栈维护,栈中装的是最右链上节点的$pos$
而我们要找的$j$,就是$val[j]<val[i]$、且最靠近栈底的元素
原理理解了之后,重新整理一下思路,尽量简单清楚地建笛卡尔树:
1. 用单调栈维护最右链
2. 每次插入当前的$i$,在单调栈中不停弹出栈顶,直到栈顶$fa$满足$val[fa]<val[i]$,则最后一次弹出的就是$j$
3. 将$i$作为$fa$的右儿子,$j$作为$i$的左儿子
是不是很简单owo
复杂度$O(n)$,是相当优秀的一种方法
#include <cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; const int N=100005; int n; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]>a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); Build(); /* for(int i=1;i<=n;i++) printf("i=%d ls=%d rs=%d\n",i,ls[i],rs[i]);*/ return 0; }
所以在一些情况下,笛卡尔树的题目可以不用建树,直接用单调栈就够了
~应用~
最简单的一个应用是求元素的左右延伸区间
具体点说,就是对于一个数列$a$,询问以$a[i]$为区间最大(小)值的最长区间
使用笛卡尔树,就可以通过$O(n)$的预处理做到$O(1)$查询:进行中序遍历,每个节点$x$的子树的$pos$最小、最大值就是答案
模板题:HDU 1506 ($Largest\ Rectangle\ in\ a\ Histogram$)
#include <cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; typedef long long ll; const int N=100005; int n; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { v.clear(); memset(ls,0,sizeof(ls)); memset(rs,0,sizeof(rs)); for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]>a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; void dfs(int x) { l[x]=r[x]=x; if(ls[x]) { dfs(ls[x]); l[x]=l[ls[x]]; } if(rs[x]) { dfs(rs[x]); r[x]=r[rs[x]]; } } int main() { scanf("%d",&n); while(n) { for(int i=1;i<=n;i++) scanf("%d",&a[i]); Build(); dfs(root); ll ans=0; for(int i=1;i<=n;i++) ans=max(ans,ll(a[i])*(r[i]-l[i]+1)); printf("%lld\n",ans); scanf("%d",&n); } return 0; }
一个稍微高级一点的应用,就是给出分治的边界
一道不错的题:Luogu P4755 ($Beautiful\ Pair$)
官方题解已经很完善了:FlierKing - 题解 P4755 【Beautiful Pair】
简单点说,就是每次取当前区间$[l,r]$的最大值$a_i$,那么$i$即为笛卡尔树中 此区间对应子树的根节点
于是将区间分成两部分$[l,i-1],[i+1,r]$的操作,就可以转化成笛卡尔树上的分治
同时,这个题解将“统计$[l,r]$中$a_i\leq x$的数量”这个主席树问题,离线后通过拆分转化为树状数组问题,设计十分巧妙
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; typedef pair<int,int> pii; const int N=100005; int n; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]<a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; inline void dfs(int x) { if(ls[x]) { dfs(ls[x]); l[x]=l[ls[x]]; } else l[x]=x; if(rs[x]) { dfs(rs[x]); r[x]=r[rs[x]]; } else r[x]=x; } vector<pii> add[N]; inline void Solve(int x) { int lp=x-l[x],rp=r[x]-x; if(lp<rp) for(int i=l[x];i<=x;i++) { add[r[x]].push_back(pii(a[x]/a[i],1)); add[x-1].push_back(pii(a[x]/a[i],-1)); } else for(int i=x;i<=r[x];i++) { add[x].push_back(pii(a[x]/a[i],1)); add[l[x]-1].push_back(pii(a[x]/a[i],-1)); } if(ls[x]) Solve(ls[x]); if(rs[x]) Solve(rs[x]); } vector<int> pos; int t[N]; inline int lowbit(int x) { return x&(-x); } inline void Add(int k,int x) { for(int i=k;i<=n;i+=lowbit(i)) t[i]+=x; } inline int Query(int k) { int res=0; for(int i=k;i;i-=lowbit(i)) res+=t[i]; return res; } int main() { scanf("%d",&n); pos.push_back(0); for(int i=1;i<=n;i++) scanf("%d",&a[i]),pos.push_back(a[i]); sort(pos.begin(),pos.end()); pos.resize(unique(pos.begin(),pos.end())-pos.begin()); Build(); dfs(root); Solve(root); ll ans=0; for(int i=1;i<=n;i++) { int p=lower_bound(pos.begin(),pos.end(),a[i])-pos.begin(); Add(p,1); for(int j=0;j<add[i].size();j++) { int lim=lower_bound(pos.begin(),pos.end(),add[i][j].first)-pos.begin(); if(pos[lim]>add[i][j].first) lim--; ans=ans+add[i][j].second*Query(lim); } } printf("%lld\n",ans); return 0; }
学完之后立马就现场碰到基本一样的题...
牛客ACM 883G ($Removing\ Stones$,2019牛客暑期多校训练营(第三场))
只不过对于当前区间,是遍历较小的那一半、并在另一半二分
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=300005; int n; int a[N]; ll p[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]<a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; inline void dfs(int x) { if(ls[x]) { dfs(ls[x]); l[x]=l[ls[x]]; } else l[x]=x; if(rs[x]) { dfs(rs[x]); r[x]=r[rs[x]]; } else r[x]=x; } ll ans=0; void Solve(int x) { ll sum=0; int lp=x-l[x]+1,rp=r[x]-x+1; int left,right,mid; if(lp<rp) for(int i=x;i>=l[x];i--) { sum+=a[i]; left=x,right=r[x]+1,mid; while(left<right) { mid=(left+right)>>1; if(sum+p[mid]-p[x]<2LL*a[x]) left=mid+1; else right=mid; } ans+=r[x]-left+1; } else for(int i=x;i<=r[x];i++) { sum+=a[i]; left=l[x],right=x,mid; while(left<right) { mid=(left+right)>>1; if(sum+p[x-1]-p[mid-1]>=2LL*a[x]) left=mid+1; else right=mid; } if(sum+p[x-1]-p[left-1]<2LL*a[x]) left--; ans+=left-l[x]+1; } if(ls[x]) Solve(ls[x]); if(rs[x]) Solve(rs[x]); } void Clear() { ans=0; v.clear(); for(int i=1;i<=n;i++) ls[i]=rs[i]=0; } int main() { int T; scanf("%d",&T); while(T--) { Clear(); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]),p[i]=p[i-1]+a[i]; Build(); dfs(root); Solve(root); printf("%lld\n",ans); } return 0; }
一道比较明显的题:HDU 6701 ($Make\ Rounddog\ Happy$,$2019\ Multi-University\ Training\ Contest\ 10$)
由于需要对$a_l,...,a_r$求max,所以能比较自然地想到笛卡尔树上分治
然后就是处理区间内数字不同的限制;不过也并不困难,只要正向、反向各扫一遍就能预处理出来$i$向左、向右不出现重复数字的最大延伸长度了
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=300005; int n,k; int a[N]; int root; int ls[N],rs[N]; vector<int> v; void Build() { v.clear(); for(int i=1;i<=n;i++) { int j=0; while(v.size() && a[v.back()]<a[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } int l[N],r[N]; void dfs(int x) { l[x]=r[x]=x; if(ls[x]) dfs(ls[x]),l[x]=l[ls[x]]; if(rs[x]) dfs(rs[x]),r[x]=r[rs[x]]; } int cnt[N]; int L[N],R[N]; ll ans=0; void Solve(int x) { int lp=x-l[x],rp=r[x]-x; if(lp<rp) { for(int i=l[x];i<=x;i++) ans+=max(0,min(R[i],r[x])-max(a[x]-k+i-1,x)+1); } else { for(int i=x;i<=r[x];i++) ans+=max(0,min(k-a[x]+i+1,x)-max(L[i],l[x])+1); } if(ls[x]) Solve(ls[x]); if(rs[x]) Solve(rs[x]); } int main() { int T; scanf("%d",&T); while(T--) { scanf("%d%d",&n,&k); ans=0; for(int i=1;i<=n;i++) ls[i]=rs[i]=0; for(int i=1;i<=n;i++) scanf("%d",&a[i]); Build(); dfs(root); int j=1; for(int i=1;i<=n;i++) { cnt[a[i]]++; while(cnt[a[i]]>1) { R[j]=i-1; cnt[a[j]]--; j++; } } for(int i=j;i<=n;i++) R[i]=n,cnt[a[i]]--; j=n; for(int i=n;i>=1;i--) { cnt[a[i]]++; while(cnt[a[i]]>1) { L[j]=i+1; cnt[a[j]]--; j--; } } for(int i=j;i>=1;i--) L[i]=1,cnt[a[i]]--; Solve(root); printf("%lld\n",ans); } return 0; }
标算是左偏树,不过用笛卡尔树+倍增也能搞过去:HDU 5575 ($Discover\ Water\ Tank$,$2015\ ACM/ICPC$上海)
首先可以根据隔板的高度,对于$n-1$个隔板建立一个大根笛卡尔树
有了这棵笛卡尔树,我们可以考虑利用它来划分出分治区间
比如,对于笛卡尔树根节点对应原序列的位置$pos_{root}$,相当于将$1\text{~}n$的区间划分成两部分$[1,pos_{root}],[pos_{root}+1,n]$,且每部分的水位最高都不超过$h[pos_{root}]$;其余节点的划分同理
我们先将每个查询分配到划分树上,具体方法是,先倍增出每个划分树节点的父亲关系,然后对于每个查询$\{x,y,w\}$,从$[x,x]$对应的区间,向上找到树上最深的 水位限制大于等于$y$的祖先,并将这个查询扔到那个节点的vector中
于是考虑树形dp
对于一个区间,要不将它整体灌满,要不不灌满、并向下递归;所以对于每个划分树上的节点,分别记录灌满和不灌满的 最多正确询问数
注意$y$的边界即可(我的处理是将每个$y++$)
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; struct Query { int x,y,w; Query(int a,int b,int c) { x=a,y=b,w=c; } }; inline bool operator <(Query A,Query B) { return A.y<B.y; } inline bool operator >(Query A,Query B) { return A.y>B.y; } const int INF=1<<30; const int N=200005; const int LOG=20; int n,m; int h[N]; struct Cartesian { int root; int ls[N],rs[N]; vector<int> v; void clear() { root=0; v.clear(); for(int i=1;i<n;i++) ls[i]=rs[i]=0; } void build() { for(int i=1;i<n;i++) { int j=0; while(v.size() && h[v.back()]<h[i]) { j=v.back(); v.pop_back(); } if(!v.size()) root=i; else rs[v.back()]=i; ls[i]=j; v.push_back(i); } } }tree; int tot; int lb[N],rb[N],lim[N]; int ls[N],rs[N]; int fa[N][LOG]; int place[N]; void Build(int x,int y,int l,int r,int f) { lb[x]=l,rb[x]=r; fa[x][0]=f; if(l==r) { place[l]=x; return; } ls[x]=++tot; lim[tot]=h[y]; Build(tot,tree.ls[y],l,y,x); rs[x]=++tot; lim[tot]=h[y]; Build(tot,tree.rs[y],y+1,r,x); } int sum[N],sub[N]; vector<Query> v[N]; void dfs(int x) { if(ls[x]) dfs(ls[x]); if(rs[x]) dfs(rs[x]); int empty=0,full=0; for(int i=0;i<v[x].size();i++) { Query tmp=v[x][i]; if(tmp.w==0) empty++; } sub[x]=sub[ls[x]]+sub[rs[x]]+empty; for(int i=0;i<v[x].size();) { int j=i; while(j<v[x].size() && v[x][i].y==v[x][j].y) { Query tmp=v[x][j]; if(tmp.w==0) empty--; if(tmp.w==1) full++; j++; } i=j; sub[x]=max(sub[x],sum[ls[x]]+sum[rs[x]]+full+empty); } sum[x]=sum[ls[x]]+sum[rs[x]]+full; } int main() { int T; scanf("%d",&T); for(int kase=1;kase<=T;kase++) { scanf("%d%d",&n,&m); for(int i=1;i<n;i++) scanf("%d",&h[i]); tree.clear(); tree.build(); for(int i=1;i<=tot;i++) { v[i].clear(); ls[i]=rs[i]=fa[i][0]=0; sum[i]=sub[i]=0; } tot=1; lim[1]=INF; Build(1,tree.root,1,n,0); for(int i=1;i<LOG;i++) for(int j=1;j<=tot;j++) fa[j][i]=fa[fa[j][i-1]][i-1]; for(int i=1;i<=m;i++) { int x,y,w; scanf("%d%d%d",&x,&y,&w); Query tmp(x,++y,w); int p=place[x]; for(int j=LOG-1;j>=0;j--) if(fa[p][j] && y>lim[fa[p][j]]) p=fa[p][j]; if(y>lim[p]) p=fa[p][0]; v[p].push_back(tmp); } for(int i=1;i<=tot;i++) sort(v[i].begin(),v[i].end()); dfs(1); printf("Case #%d: %d",kase,sub[1]); putchar('\n'); } return 0; }
一般都是银牌题难度的样子吧,平常见的不多,遇到再补充
(完)