笛卡尔树
这篇文章有一部分是 2021 年写的,当时完全没有理解笛卡尔树的本质。
笛卡尔树就是最值分治的搜索树。所以最容易的构建笛卡尔树的方式为:
int get(int l,int r){
// 返回 [l,r] 中最值对应的下标
}
void solve(int p,int l,int r){
if(l>=r)return;
solve(lc[p]=get(l,p-1),l,p-1);
solve(rc[p]=get(p+1,r),p+1,r);
}
笛卡尔树的定义#
笛卡尔树是一种二叉树,其中的每个节点的权值为 。其中,单看权值 ,这是一棵二叉搜索树。单看权值 ,这是一个堆。
如果在序列 上建立笛卡尔树,每个点的权值为 ,那么一个节点实际上代表了它子树所覆盖的区间的最值。这是笛卡尔树最常见的性质。
笛卡尔树的建立#
上面的构建方法较为常见,用 ST 表维护最值。不过下面的方法更简洁,并且是线性的。
使用一个栈构建笛卡尔树。构建时,需要知道每个点的权值 ,以 为关键字将点排序,并按顺序将点插入笛卡尔树中。
很明显,一个点插入后,在下一个点插入之前,这个点在这棵树的右链末端,因为此时这个点的 是整棵树中最大的。其中,右链指的是从根节点出发,一直往右走形成的链。
为了维护笛卡尔树的堆性质,我们将右链上的点保存在一个栈中,通过扫描栈的方式决定新加入的点应该插入到哪一个位置。
具体地,设当前栈存储了从当前树的根节点到右链末尾的点。不断弹出栈顶,直至发现栈顶的 值较将要加入的节点小,则此时栈顶可以作为新加入的节点的父亲。这时,我们把栈顶的右子树接在新加入的点的左子树位置上,并把新加入的点作为栈顶的右儿子,新点入栈。
这个过程成功地维护了保存右链的栈,它的时间复杂度是 ,这是因为每个点至多出入栈分别一次。代码见例题中的模板代码。
笛卡尔树的练习#
P5854 【模板】笛卡尔树#
板子题,没什么好讲的。对于序列中第 个点,其权值就是 ,所以不需要排序了。
直接放核心代码:
const int N=1e7+5;
int n,a[N],sta[N],l[N],r[N],top;
ll ans1,ans2;
int main(){
IO::rd(n);
for(int i=1;i<=n;i++){
IO::rd(a[i]);
while(top&&a[sta[top]]>a[i])top--;
l[i]=r[sta[top]];//注意这里 r[0] 代表当前的根,所以这样写没有问题。
r[sta[top]]=i;
sta[++top]=i;
}
for(ll i=1;i<=n;i++){
ans1^=i*(l[i]+1);
ans2^=i*(r[i]+1);
}
IO::wt(ans1,' ');IO::wt(ans2,'\n');
return 0;
}
P1377 [TJOI2011]树的序#
题意:给定大小为 的一棵 BST 的生成序列,求可以生成同样形态 BST 的字典序最小的生成序列。。
思考生成序列对于 BST 形态的影响。可以想到,一个节点在生成序列中的下标总是大于它父亲在生成序列中的下标。也就是这个 BST 在生成序列下标意义下满足小根堆性质。也就是说,这个笛卡尔树中每个点的权值为 。我们可以通过建立笛卡尔树的方式快速建立这个 BST。
然后我们考虑如何求出最小字典序的生成序列。为了让新的生成序列仍然满足小根堆性质,我们应该在遍历到一个点时,先将这个点加入生成序列末尾。然后因为左子树的键值比较小,先遍历左子树。也就是输出这棵树的先序遍历。
核心代码:
const int N=1e5+5;
int n,val[N],a[N],sta[N],ls[N],rs[N],top;
inline bool cmpA(const int&A,const int&B){
return val[A]<val[B];
}
void dfs(int u){
if(!u)return;
IO::wt(u,' ');
dfs(ls[u]);dfs(rs[u]);
}
int main(){
IO::rd(n);
for(int i=1;i<=n;i++){
IO::rd(val[i]);
a[i]=i;
}
sort(a+1,a+n+1,cmpA);
for(int i=1;i<=n;i++){
while(top&&a[sta[top]]>a[i])top--;
ls[i]=rs[sta[top]];
rs[sta[top]]=i;
sta[++top]=i;
}
dfs(sta[1]);
return 0;
}
SP1805 Largest Rectangle in a Histogram#
这是一个单调栈的经典例题,也可以用笛卡尔树做。
刚看到这道题很容易产生一个想法:遍历每一个矩形,把当前矩形高度当成答案矩形高度,并将答案往两边拓展。设当前矩形为 ,,。则答案为 。
考虑构建一棵节点权值为 的笛卡尔树,其中 满足小根堆性质。这样一来,对于任意一个点 的子树内的节点 ,都有 。
设子树 的大小为 ,则答案为 。
核心代码:
const int N=1e5+5;
int n,h[N],sta[N],ls[N],rs[N],siz[N],top;
ll ans;
void dfs(int u){
if(!u)return;
dfs(ls[u]);dfs(rs[u]);
siz[u]=siz[ls[u]]+siz[rs[u]]+1;
ans=max(ans,(ll)h[u]*siz[u]);
}
int main(){
while(1){
IO::rd(n);
if(!n)break;
ans=0;top=0;
for(int i=0;i<=n;i++){//由于 rs[0] 代表根,所以也要清零。
ls[i]=rs[i]=siz[i]=0;
}
for(int i=1;i<=n;i++){
IO::rd(h[i]);
while(top&&h[sta[top]]>=h[i])top--;
ls[i]=rs[sta[top]];
rs[sta[top]]=i;
sta[++top]=i;
}
dfs(sta[1]);
IO::wt(ans,'\n');
}
return 0;
}
CF1748E Yet Another Array Counting Problem#
水题,直接在笛卡尔树上跑 DP 就行了。
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
using namespace std;
typedef long long ll;
constexpr int N=2e5+5,P=1e9+7;
int n,m,f[N][20],a[N];
inline int ask(int l,int r){
int k=__lg(r-l+1),x=f[l][k],y=f[r-(1<<k)+1][k];
return a[x]>=a[y]?x:y;
}
inline ll add(const ll&x,const ll&y){return (x+y)%P;}
vector<ll>dfs(int l,int r){
if(l==r)return vector<ll>(m,1);
int p=ask(l,r);vector<ll>ret(m),L,R;
if(l<p){
L=dfs(l,p-1);
partial_sum(all(L),L.begin(),add);
}else L=vector<ll>(m,1),ret[0]=1;
if(r>p){
R=dfs(p+1,r);
partial_sum(all(R),R.begin(),add);
}else R=vector<ll>(m,1);
for(int i=1;i<m;i++)ret[i]=L[i-1]*R[i]%P;
ret[0]=ret[0]*R[0]%P;
return ret;
};
inline void ct(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];f[i][0]=i;
}
for(int j=1;j<=__lg(n);j++){
for(int i=1;i+(1<<j)-1<=n;i++){
int x=f[i][j-1],y=f[i+(1<<(j-1))][j-1];
f[i][j]=(a[x]>=a[y]?x:y);
}
}
vector ans=dfs(1,n);
cout<<accumulate(all(ans),0ll,add)<<'\n';
}
int main(){
int T;cin>>T;while(T--)ct();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现