Cartesian 树(笛卡尔树)

一部分人称呼笛卡尔,一部分人称呼笛卡儿,于是把英译放上去消歧义用。

下文取笛卡尔树作名字。

1. 是什么

笛卡尔树是一种二叉搜索树,Treap 是它的延申,两种树有一样的构造原理。

具体来说,我们要求将一些元素组织成一棵笛卡尔树,这些元素都有两个值 \(key , val\)。在构建好的笛卡尔树上,各元素的 \(key\) 按照二叉搜索树的规则排列(左子树上的所有点的 \(key\) \(<\) 根节点的 \(key\) \(<\) 右子树上所有点的 \(key\));各元素的 \(val\) 按照堆的性质排列(小根堆或者大根堆)。

一般来说,\(key\) 应互不相同,因此代码实现中通常用元素下标当作 \(key\)。若有元素 \(val\) 值相同,按照 \(key\) 的顺序排列。

笛卡尔树不属于平衡树,令节点数为 \(n\),树高可以达到 \(O(n)\)

2. 怎么建

分治法

首先对所有元素按照 \(key\) 排序,我们以小根堆的笛卡尔树为例,找到全局最小值的位置,假设其在第 \(k\) 位。接下来递归去找 \([1,k-1]\) 区间和 \([k+1,n]\) 区间的最小值所在的位置,并令左部分的位置位 \(k\) 的左儿子,右部分的位置位 \(k\) 的右儿子,若不存在则为 \(0\)。由于我们已经对 \(key\) 排序,这样的连接后 \(key\) 仍有二叉搜索树的性质。连接完成后,返回 \(k\) 给更上级的函数使用。

这是个分治过程,如果用线段树维护区间最值的位置,时间复杂度为常数比较大的 \(O(n \log n)\)

但还是不够快,若 \(key\) 天然有序(如用元素下标当作 \(key\)),我们有 \(O(n)\) 的构造方法。

单调栈

还是以小根堆的笛卡尔树为例,在单调栈中,我们让 \(val\) 从底至顶升序排序,并以 \(key\) 从小到大尝试将元素加入笛卡尔树和单调栈。

现在有一个新元素要加入笛卡尔树,分下情况:

  • 如果新元素不会导致弹栈(\(val\) 比栈顶的 \(val\) 还大),因为先加入的元素的 \(key\) 一定小于当前元素的 \(key\),因此当前元素应作为栈顶元素的右儿子。
  • 如果新元素直接导致栈弹完,我们取最后一个弹出的元素,将其作为当前元素的左儿子,因为其 \(key\) 都小于当前元素的 \(key\)
  • 如果导致弹栈但并未弹完,让最后一个弹出的元素作为当前元素的左儿子,让当前元素作为栈顶元素的右儿子。

每次操作完成后,都要把当前元素压入栈中。

显然,此方法的时间复杂度为 \(O(n)\)

3. 怎么用

先来写写模板吧

P5854 【模板】笛卡尔树

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e7+444;
int val[N];
stack<int> s;
struct tree{
  int ls,rs;
}t[N];
int main(){
  
  int n;cin>>n;
  for(int i=1;i<=n;i++){
    val[i]=rd;
    int lst=-1;
    while(s.size()&&val[s.top()]>val[i])lst=s.top(),s.pop();
    if(s.size()==0&&lst==-1){
      s.push(i);
    }
    else if(s.size()==0){
      t[i].ls=lst;
      s.push(i);  
    }
    else if(lst==-1){
      t[s.top()].rs=i;
      s.push(i);  
    }
    else{
      t[s.top()].rs=i;
      t[i].ls=lst;
      s.push(i);
    }
  }
  ll la=0,ra=0;
  for(ll i=1;i<=n;i++){
    la=la^(i*(t[i].ls+1));
    ra=ra^(i*(t[i].rs+1));
  }
  cout<<la<<' '<<ra;
  
  return 0;
}

P1377 [TJOI2011] 树的序

笛卡尔树可以加速构造二叉搜索树。

所有生成序列中最小的一种就是在建好的二叉搜索树上跑下前序遍历,但是要干的事是快速构造二叉搜索树。

这题输入的是二叉搜索树的 \(key\),于是把输入值放到下标去,把下标放到值去再构造笛卡尔就好了。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e5+5;
int arr[N];
stack<pair<int,int> >s; 
struct tree{
  int ls,rs;
}t[N];
int root;
void ft(int u){
  cout<<u<<' ';
  if(t[u].ls!=0)ft(t[u].ls);
  if(t[u].rs!=0)ft(t[u].rs);
}
int main(){
  
  int n;cin>>n;
  for(int i=1;i<=n;i++){
    int val;cin>>val;
    arr[val]=i;
  }
  for(int i=1;i<=n;i++){
    pair<int,int> lst=mkp(-1,-1);
    while(s.size()&&arr[s.top().second]>arr[i])lst=s.top(),s.pop();
    if(s.size()==0&&lst==mkp(-1,-1)){
      s.push(mkp(arr[i],i));
      root=i;
    }
    else if(s.size()==0){
      t[i].ls=lst.second;
      s.push(mkp(arr[i],i));  
      root=i;
    }
    else if(lst==mkp(-1,-1)){
      t[s.top().second].rs=i;
      s.push(mkp(arr[i],i));  
    }
    else{
      t[s.top().second].rs=i;
      t[i].ls=lst.second;
      s.push(mkp(arr[i],i));
    }
  }
  ft(root);
  
  return 0;
}

CF1748E

笛卡尔树可以有效处理序列上最大,最小值的位置问题,并把序列按照最值拆成树。

该问题中,我们以下标作为 \(key\),权值为 \(val\),此时建出的笛卡尔树中节点的意义即为:如果选择的区间经过了这个点 \(key\) 对应的位置,那么题目中要求的所谓 “最左端最大值位置” 就一定得在 \(key\) 位置。

于是,我们可以从根节点开始,从 \(1\)\(k\) 尝试向根节点中填数,接着向两儿子递归式的计算,此时对于一个子树的根,设其被填入的树为 \(k_i\),那么对于这个子树对应的区间的方案数即为左儿子填 \([1,k_i-1]\) 的方案数与右儿子填 \([1,k_i]\)(不 \(-1\) 是因为只关心最左端最大值)的方案数之积。

对于 \([1,k]\) 的方案数,显然是可以前缀和的。于是就做完了,时间复杂度为 \(O(nm)\)

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e5+5;
int val[N]; 
stack<int> s;
int root=0;
struct tree{
  int ls,rs;
}t[N];
const int mod=1e9+7;
int dp[N];
ll dfs(int u,int filt){
  ll al,ar;
	if(t[u].ls==t[u].rs&&t[u].rs==0){
		dp[u]=max(0,filt);
		return dp[u];
	} 
  if(t[u].ls==0) al=1;
  else al=dfs(t[u].ls,filt-1);

  if(t[u].rs==0) ar=1;
  else ar=dfs(t[u].rs,filt);

  dp[u]=(dp[u]+al*ar%mod)%mod;
  return dp[u];
}
int main(){
  
  int tc;cin>>tc;
  while(tc--){
    memset(t,0,sizeof t);
    memset(dp,0,sizeof dp);
    memset(val,0,sizeof val);
    root=0;
    while(s.size())s.pop();
    int n,m;cin>>n>>m;
    for(int i=1;i<=n;i++){cin>>val[i];}
    for(int i=1;i<=n;i++){
      int lst=-1;
      while(s.size()&&val[s.top()]<val[i])lst=s.top(),s.pop();
      if(s.size()==0&&lst==-1){
        s.push(i);
        root=i;
      }
      else if(s.size()==0){
        s.push(i);
        t[i].ls=lst;
        root=i;
      }
      else if(lst==-1){
        t[s.top()].rs=i;
        s.push(i);
      }
      else{
        t[s.top()].rs=i;
        t[i].ls=lst;
        s.push(i);
      } 
    }
    ll ans=0;
    for(int i=1;i<=m;i++){
      ans=dfs(root,i);
    }
    cout<<ans<<'\n';
  }
  
  return 0;
}

P6453 [COCI 2008/2009 #4] PERIODNI

最值决定了管理的区块范围,通过笛卡尔树将原本不规则的图形拆分成由高度决定的矩形块,再树上 DP 解决问题。

同时也是一道 DP 好题。

首先来几个简单的,考虑一个 \(k\times k\) 的棋盘,向里面放入 \(k\) 个相同的数,不得有任意两个数在同一行或者同一列的方案?

这个简单,就是 \(k!\)

下面,考虑一个 \(n\times m\) 的棋盘,向里面放入 \(k\) 个相同的数,不得有任意两个数在同一行或者同一列的方案?

我们尝试用组合数把这个 \(n\times m\) 的棋盘分成几个 \(k\times k\) 的棋盘。具体的,我们从 \(n\) 行中选出 \(k\) 行;\(m\) 列中选出 \(k\) 列,他们相交于 \(k\times k\) 个格子,于是就可以把他们拼成一个 \(k\times k\) 的棋盘。

所以总方案数为:

\[\binom{n}{k} \times \binom{m}{k} \times k! \]

有意思

回到题目,首先让高度当 \(val\) 下标当 \(key\) 建笛卡尔树,此时从根节点出发,求解出每个点对应的高度可以管的范围和高度。

这个我不是很会描述啊,总的来说就是向左走限制右端点,向右走限制左端点。这个在笛卡尔树建好之后就是一个挺好的性质,手摸一下就可以。

然后这个点管辖的高度由它的父亲决定。

接下来主要思想就是从上到下合并矩形块,对应在笛卡尔树上从下往上 DP,对于每个点,计算放 \(k\) 个数的方案数。有很好的的一点是上面的数字怎么放只会影响下面行的选择,在组合数里减去即可。

这里分下情况,如果这个点只有一个儿子,枚举合并的整块里放多少个,枚举儿子里放了多少个,再用组合数计算自己放的方案数,乘起来即可。

如果有两个儿子,这意味着这个块一定作为一个类似桥梁的东西连接左右儿子代表的两个隔着的矩形块。

此时有个小技巧,我们先把两个儿子代表的块合成一个大块。这个好办,因为不相邻,方案数乘起来即可。

之后再用这个大块去和当前根合并,就很简单了。

于是做完了。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=600;
const int mod=1e9+7;
const int PN=1e6+6;
int he[N];
struct tree{
  int ls,rs;
}t[N];
int n,k;
stack<int> s;
int root=0;
int len[N],hei[N];
ll dp[N][N];
ll jc[PN];
ll x,y;
ll exgcd(ll a,ll b,ll &x,ll &y){
	if(b==0){
		x=1;
		y=0;
		return a;
	}
	ll d=exgcd(b,a%b,y,x);
	y=y-a/b*x;
	return d;
}
ll C(ll n,ll m){
  if(m>n||n<0)return 0;
	ll upp=jc[n-m]*jc[m]%mod;
	ll d=exgcd(upp,mod,x,y);
	while(x<=0){
		x+=mod/d;
	}
	x=x%(mod/d);
	return jc[n]*x%mod;
}  
void dfs(int p,int l,int r,int lh){
  len[p]=r-l+1;
  hei[p]=abs(he[p]-lh);
  if(t[p].ls!=0){dfs(t[p].ls,l,p-1,he[p]);}
  if(t[p].rs!=0){dfs(t[p].rs,p+1,r,he[p]);}
  return ;
}
void dpr(int p){
  if(t[p].ls==t[p].rs&&t[p].ls==0){
    for(int i=0;i<=k;i++){
      if(i==0){dp[p][i]=1;continue;}
      dp[p][i]=C(len[p],i)*C(hei[p],i)%mod*jc[i]%mod;
    }
    return ;
  }
  else if(t[p].ls==0){
    dpr(t[p].rs);
    for(int i=0;i<=k;i++){
      if(i==0){dp[p][i]=1;continue;}
      for(int wo=0;wo<=i;wo++){
        dp[p][i]=(dp[p][i] + (dp[t[p].rs][i-wo] *C(len[p]-(i-wo),wo)%mod *C(hei[p],wo)%mod *jc[wo]%mod) )%mod;
      }
    }
  }
  else if(t[p].rs==0){
    dpr(t[p].ls);
    for(int i=0;i<=k;i++){
      if(i==0){dp[p][i]=1;continue;}
      for(int wo=0;wo<=i;wo++){
        dp[p][i]=(dp[p][i] + (dp[t[p].ls][i-wo] *C(len[p]-(i-wo),wo)%mod *C(hei[p],wo)%mod *jc[wo]%mod) )%mod;
      }
    }
  }
  else{

    dpr(t[p].ls); dpr(t[p].rs);

    ll align[N];memset(align,0,sizeof align);
    for(int i=0;i<=k;i++){
      if(i==0){align[i]=1;continue;}
      for(int ni=0;ni<=i;ni++){
        align[i]=(align[i]+ dp[t[p].ls][ni]*dp[t[p].rs][i-ni]%mod )%mod;
      }
    }

    for(int i=0;i<=k;i++){
      if(i==0){dp[p][i]=1;continue;}
      for(int wo=0;wo<=i;wo++){
        dp[p][i]=(dp[p][i] + (align[i-wo] *C(len[p]-(i-wo),wo)%mod *C(hei[p],wo)%mod *jc[wo]%mod) )%mod;
      }
    }

  }
}
int main(){
  cin>>n>>k;
  for(int i=1;i<=n;i++){
    cin>>he[i];
    int lst=-1;
    while(s.size()&&he[s.top()]>he[i])lst=s.top(),s.pop();
    if(s.size()==0&&lst==-1){
      s.push(i);
      root=i;
    }
    else if(s.size()==0){
      s.push(i);
      t[i].ls=lst;
      root=i;
    }
    else if(lst==-1){
      t[s.top()].rs=i;
      s.push(i);
    }
    else{
      t[s.top()].rs=i;
      t[i].ls=lst;
      s.push(i);
    }
  }
  jc[0]=1;
  for(int i=1;i<=1e6;i++){
  	jc[i]=jc[i-1]*i%mod;
  }
  dfs(root,1,n,0);
  dpr(root);
  cout<<dp[root][k];

  return 0;
}

AGC028B Removing Blocks

考虑点的贡献,以删除顺序升序建笛卡尔树,该点贡献即为笛卡尔树上该点的祖先数量,进而转化成该点的深度。

借助笛卡尔树的性质推式子,计算各点成为该点的祖先的概率,得到该点深度的期望(人话就是 \(n!\) 种情况的平均值),乘以 \(n!\) 和点权并对所有的点这样求和即为答案。

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=2e5+9;
ll inv[N]; 
ll am[N];
ll jc[N];
ll pre[N];
const int mod=1e9+7;
int main(){
	
	ll n;cin>>n;
  for(int i=1;i<=n;i++)cin>>am[i];
	inv[1]=1;jc[0]=1;jc[1]=1;
	for(ll i=2;i<=n;i++){
		inv[i]=-(mod/i)*inv[mod%i];
		while(inv[i]<0)inv[i]+=mod;
		inv[i]%=mod;jc[i]=jc[i-1]*i;
	}
  for(int i=1;i<=n;i++)pre[i]=(pre[i-1]+inv[i])%mod;
  ll ans=0;
  for(int i=1;i<=n;i++)ans+=((pre[i]+pre[n-i+1]-1)%mod*am[i])%mod,ans%=mod;
  for(int i=1;i<=n;i++)ans=(ans*i)%mod;
  cout<<ans;

	return 0;
} 
posted @ 2025-07-13 18:44  hm2ns  阅读(401)  评论(0)    收藏  举报