听上去有丶厉害,实际也很巧妙
学习了这两篇: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; }
(待续)