笛卡尔树 学习笔记

概念

笛卡尔树 是一种Treap。每个节点有两个值,分别为键值和权值。以插入顺序(时间戳)为键值,如果只看键值,则符合BST(二叉搜索树)的性质。如果只看权值,则符合堆的性质。(此处可以是大根堆或小根堆,以下只讨论小根堆)

在一个序列中,我们通常以下标顺序从小到大插入每一个元素,这样可以建出一棵笛卡尔树。或者说,我们可以把数按插入顺序记录成一个数组。

(oiwiki上的图)

性质

  1. 根据BST的性质,每个节点的右儿子子树内的节点在序列中都在该节点右侧,左儿子子树同理。
  2. 节点 \(u,v\) 在笛卡尔树中的最近公共祖先 \(lca(u,v)\) 是 序列中区间 \([u,v]\) 中值最小的位置。
  3. 笛卡尔树建树是 \(O(n)\) 的,非常快。
  4. 在随机数据下(或者说一个随机序列),笛卡尔树的树高期望在 \(O(\log n)\)。

建树

(以下操作以最小值建的笛卡尔树(权值满足小根堆性质)为例,最大值同理。先默认所有权值互不相同)

本质上,笛卡尔树可以这么理解,不断将一个区间 \([l,r]\) 找到其最小值的位置 \(x\),然后将该区间分为三个部分:\([l,x-1],x,[x+1,r]\)。也就是不断找最小值,然后分割区间。

所以根据上述理解,我们可以通过st表在 \(O(n\log n)\) 的时间复杂度。

但是能不能更快呢?

如果我们按顺序插入每一个值,因为要满足插入顺序符合BST的性质,所以我们新加入的数有两种情况(有可能两种都满足):

  1. 是某个已加入的点的右儿子。
  2. 这个新加的点的左儿子是某个已加入的点。

而又因为我们只会在当前树的右边加入新节点(无论是第一种还是第二种,都是加在树的右边),只跟树的最右边的点有关,所以考虑维护一个右链。

这里右链指的是从根节点开始,不断跳右儿子,直到没有右儿子为止,所经过的点顺序构成的序列。

显然,根据小根堆的性质,这个右链上的点的权值单调不降。

再考虑权值:

设 \(w_u\) 为 \(u\) 的权值,某个已加入的数为 \(x\),新加的点为 \(u\)。

则第一种对应的就是 \(w_x< w_u\),第二种为 \(w_x> w_u\)。因为右链上的点权值单调不降,我们一定能找到深度最深的点 \(v\) 满足 \(w_v<w_u\),且右链上 \(v\) 的后一个点为 \(k\)(\(k\) 可能不存在)。

那么就让 \(u\) 成为 \(v\) 的右儿子,让 \(k\) 成为 \(u\) 的左儿子。

因为新加入了一个点,树的形态发生更改,所以右链也会发生变化。会发现 \(k\) 及 \(k\) 以后的右链上的点全部不在右链上了。

如何维护右链呢?发现每一次删去的都是一个后缀,且权值具有单调性,所以用单调栈维护。

(圆圈内为节点编号,圆圈外为权值)

那么总体做法就是:

用单调栈维护右链。从后往前遍历单调栈,找到第一个 \(v\) ,满足 \(w_v<w_u\),将 \(v\) 后的单调栈内的元素全部弹出。将 \(v\) 的右儿子赋值为 \(u\),将 \(u\) 的左儿子赋值为最后一个弹出的元素(没有就为 \(0\)),将 \(u\) 放入栈内。

代码实现:

for(int i=1;i<=n;i++)
{
	int las=0;
	while(top>0&&a[st[top]]>=a[i]) las=st[top],st[top]=0,top--;
	l[i]=las;
	r[st[top]]=i;
	st[++top]=i;
}

应用/题目特征

需要用最小值或最大值分割序列,或者答案跟最值有关。


例题

CF2048F Kevin and Math Class

首先可以发现一个结论,就是如果我们想让一段区间全部除以 \(a_x\),则该区间为 \([往左第一个小于a_x的位置+1,往右第一个小于a_x的位置-1]\),这样是不劣的。

还有一个结论就是 \(\lceil\frac{\lceil\frac{n}{a}\rceil}{b}\rceil=\lceil\frac{n}{ab}\rceil=\lceil\frac{\lceil\frac{n}{b}\rceil}{a}\rceil\),也就是说除的结果跟除的顺序没有关系。

所以我们相当于将该序列用若干个区间覆盖起来了。比如说 \(a_x=\min_{i=1}^na_i\),则可以让区间 \([1,n]\) 进行若干次除以 \(a_x\),而后将该序列分割为 \([1,x-1]\) 和 \([x+1,n]\) 再进行操作。

这样就有一个类似用最小值分割序列的操作。考虑建笛卡尔树。

在笛卡尔树上的一个节点 \(u\) 进行操作,会让 \(u\) 子树里所有点的值除以 \(a_u\) 向上取整。

所以考虑树形dp。设 \(dp_{i,k}\) 表示将以 \(i\) 为根的子树经过操作后最大值为 \(k\) 的最小操作数。

但是状态数为 \(2\times 10^5\times 10^{18}\),会炸而且有很多无用状态。

但是题目给了 \(b_i\ge 2\),所以总的操作次数不会超过 \(64\),因为你可以取 \(\min_{i=1}^n a_i\) 取 \(64\) 次,这样就全为 \(1\) 了。所以将状态与答案交换,则设 \(dp_{i,j}\) 为将以 \(i\) 为根的子树进行 \(j\) 次操作后最大值最小是多少。

转移就是:

\[dp_{i,j}=\min_{x=0}^{j}\min_{y=0}^{j-x} \lceil\frac{\max(a_i,dp_{lson,x},dp_{rson,y})}{b_i^{j-x-y}}\rceil \]

这样转移是 \(O(n\log^3 V)\),其中 \(V=10^{18}\) ,是值域。过不了。

考虑另外记一个数组 \(g_{i,j}\) 表示不考虑 \(a_i\) ,其他意义同 \(dp_{i,j}\)。换句话说,也就是合并左右两子树的结果。

所以有转移:

\[g_{i,j}=\min_{x=0}^{j}\max(dp_{lson,x},dp_{rson,j-x})\\dp_{i,j}=\min_{x=0}^{j}\lceil\frac{\max(a_i,g_{i,x})}{b_i^{j-x}}\rceil \]

这样就是 \(O(n\log^2 V)\) 的了。

但是出题人并不想让我们这么就过了,于是他卡了常。

会发现除法非常占时间,还要进行 \(O(n\log^2 V)\) 次?那不炸了?

那能不能少做一点?

会发现 \(dp_{i,j}\) 与 \(dp_{i,j-1}\) 的差距挺小的,所以可不可以递推?

\[dp_{i,j}=\min(\lceil\frac{dp_{i,j-1}}{b_i}\rceil,\max(a_i,g_{i,j})) \]

这样只用做 \(O(n\log V)\) 次除法了。

答案就是最小的 \(x\) ,使得 \(dp_{root,x}\le 1\)。

时间复杂度 \(O(n\log^2 V )\),常数小。

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
#define int long long
int n,a[N],st[N],l[N],r[N],b[N],top;
int dp[N][70];
int g[N][70];
int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
int up(int a,int b)
{
	if(a%b==0) return a/b;
	return a/b+1;
}
void dfs(int u)
{
	if(l[u]) dfs(l[u]);
	if(r[u]) dfs(r[u]);
	if(!l[u]&&!r[u])
	{
		dp[u][0]=a[u];
		for(int i=1;i<=64;i++)
		{
			dp[u][i]=up(dp[u][i-1],b[u]);
		}
		return;
	}
	for(int i=0;i<=64;i++)
	{
		for(int j=0;j<=64-i;j++) g[u][i+j]=min(g[u][i+j],max(dp[l[u]][i],dp[r[u]][j]));
	}
	dp[u][0]=max(a[u],g[u][0]);
	for(int i=1;i<=64;i++)
	{
		dp[u][i]=min(max(a[u],g[u][i]),up(dp[u][i-1],b[u]));
	}
}
void init()
{
	for(int i=1;i<=n;i++) 
	{
		l[i]=r[i]=0;
		for(int j=0;j<=64;j++) dp[i][j]=1e18,g[i][j]=1e18;
	}
	for(int i=1;i<=top;i++) st[i]=0;
	top=0;
}
void solve()
{
	n=read();
	init();
	int minn=1e18,pos=0;
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=n;i++) 
	{
		b[i]=read();
		if(b[i]<=minn) minn=b[i],pos=i;
	}
	
	for(int i=1;i<=n;i++)
	{
		int las=0;
		while(top>0&&b[st[top]]>=b[i]) las=st[top],st[top]=0,top--;
		l[i]=las;
		r[st[top]]=i;
		st[++top]=i;
	}
	dfs(pos);
	for(register int i=0;i<=64;i++) 
	{
		if(dp[pos][i]<=1)
		{
			printf("%d\n",i);
			return;
		}
	}
}
signed main()
{
	int T=read();
	while(T--) solve();
	return 0;
}
posted @ 2025-05-28 20:24  Daylighten  阅读(50)  评论(0)    收藏  举报