【学习笔记】笛卡尔树
前言
本来早就该学笛卡尔树了,但暑假打模拟赛就一直没学成。于是就打算先不学了,结果又发现后面有个笛卡尔树专题,只好来学学。
定义
笛卡尔树是一棵二叉树,每个点有一个键和一个值,键满足堆的性质,值满足二叉搜索树的性质。没错当键随机时,这就是个 Treap。
建树
如果值单调递增,那么就可以线性建树。具体地,维护整棵树的右链。右链就是从根开始不停往右儿子走的链。因为值递增所以新加的点一定会在右链中。假设新加入的点 \(u\) 值为 \(k\) 键为 \(w\),记为 \((k,w)\),则从右链下端开始不断向上比较,找到一个 \(x\) 使它的键小于 \(w\)。那么 \(u\) 就成为 \(x\) 的右儿子,\(x\) 原本的右儿子就成为 \(u\) 的左儿子。
放张图更直观(红色方框即为右链):
图中的数字就是相应的点的键。显然用单调栈来维护右链就行了。
核心代码:
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
for(int i=1;i<=n;i++){
while(top&&a[zhan[top]]>a[i]){
top--;
}
int &tmp=top?rs[zhan[top]]:rt;
ls[i]=tmp;
tmp=i;
zhan[++top]=i;
}
代码中值为 \(i=1\dots n\),对应的键为 \(a_i\)。
例题
[luogu5854]笛卡尔树模板
按如上方式建树即可。值得一提的是通过这道题可以发现:当键与值都互不相同时,笛卡尔树的形态是唯一的。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e7+5;
int n,a[maxn],rt;
int zhan[maxn],top;
int ls[maxn],rs[maxn];
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
read(n);
for(int i=1;i<=n;i++){
read(a[i]);
}
for(int i=1;i<=n;i++){
while(top&&a[zhan[top]]>a[i]){
top--;
}
int &tmp=top?rs[zhan[top]]:rt;
ls[i]=tmp;
tmp=i;
zhan[++top]=i;
}
ll ans1=0,ans2=0;
for(int i=1;i<=n;i++){
ans1^=i*1ll*(ls[i]+1);
ans2^=i*1ll*(rs[i]+1);
}
printf("%lld %lld",ans1,ans2);
return 0;
}
}
int main(){return asbt::main();}
[TJOI2011] 树的序
观察原树的建树方式,发现先加的一定在后加的上面。换句话说对于题中给出的 \(k_i\),下标 \(i\) 为键而 \(k_i\) 为值。因此先将 \(k\) 排序,用笛卡尔树的方式线性建树。
然后我们要找到能建出相同树的序列,必然也是从上向下建的,而我们又想要字典序最小,这还是一棵二叉搜索树,因此必然要先输出根,再遍历左子树,然后再遍历右子树。一个 dfs
即可。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e5+5;
int n,a[maxn],p[maxn];
int rt,ls[maxn],rs[maxn];
int top,zhan[maxn];
il void dfs(int u){
if(!u){
return ;
}
printf("%d ",u);
dfs(ls[u]);
dfs(rs[u]);
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
read(n);
for(int i=1;i<=n;i++){
read(a[i]);
p[i]=i;
}
sort(p+1,p+n+1,[](const int &x,const int &y){return a[x]<a[y];});
for(int i=1;i<=n;i++){
// cout<<p[i]<<"\n";
while(top&&p[zhan[top]]>p[i]){
top--;
}
int &tmp=top?rs[zhan[top]]:rt;
ls[i]=tmp;
tmp=i;
zhan[++top]=i;
}
// for(int i=1;i<=n;i++){
// cout<<i<<" "<<ls[i]<<" "<<rs[i]<<"\n";
// }
dfs(rt);
return 0;
}
}
int main(){return asbt::main();}
[hdu6305]RMQ Similar Sequence
首先,对 \(A\) 数组建一个满足大根堆性质的笛卡尔树。
这时候你发现,\(RMQ(A,l,r)\) 就是 \(l\) 和 \(r\) 的 \(lca\)。
因此如果也类似地给 \(B\) 建出笛卡尔树,这两棵树一定是相同的。因为 \(B\) 的和的期望是确定的,即 \(\frac{n}{2}\),因此只需计算一个笛卡尔树与 \(A\) 的笛卡尔树相同的概率就行了。
显然对于每棵子树,只要根都是相同的,则树就一定是相同的。设 \(sz\) 为子树大小,则概率为 \(\frac{1}{\prod{sz_i}}\)。因此答案为\(\frac{n}{2\prod{sz_i}}\)。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=1e6+5,mod=1e9+7;
int T,n,a[maxn],rt;
int zhan[maxn],top;
int ls[maxn],rs[maxn];
int inv[maxn],sz[maxn];
il void dfs(int u){
// cout<<u<<"\n";
if(!u){
return ;
}
dfs(ls[u]);
dfs(rs[u]);
sz[u]=sz[ls[u]]+sz[rs[u]]+1;
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
inv[1]=1;
for(int i=2;i<=1e6;i++){
inv[i]=(mod-mod/i)*1ll*inv[mod%i]%mod;
}
// for(int i=1;i<=1e6;i++){
// cout<<i*1ll*inv[i]%mod<<"\n";
// }
read(T);
while(T--){
read(n);
top=rt=0;
for(int i=1;i<=n;i++){
read(a[i]);
while(top&&a[zhan[top]]<a[i]){
top--;
}
int &tmp=top?rs[zhan[top]]:rt;
ls[i]=tmp;
tmp=i;
zhan[++top]=i;
}
dfs(rt);
int ans=inv[2]*1ll*n%mod;
for(int i=1;i<=n;i++){
ans=ans*1ll*inv[sz[i]]%mod;
}
printf("%d\n",ans);
for(int i=1;i<=n;i++){
ls[i]=rs[i]=sz[i]=0;
}
}
return 0;
}
}
int main(){return asbt::main();}
[洛谷 P6453]PERIODNI
考虑如果 \(a_i\) 很小,那 \(i\) 两边的高于 \(a_i\) 的部分就成了两个独立的子问题。因此可以建立小根堆笛卡尔树,然后进行树形 DP。
设 \(f_{u,i}\) 表示 \(u\) 的子树中高于 \(a_{fa_u}\) 的地方放了 \(i\) 个点的方案数。显然答案为 \(f_{rt,m}\)。
考虑转移。首先是高于 \(a_u\) 的部分,显然可以通过树上背包转移:
然后是 \((a_{fa_u},a_u]\) 的部分。设要放 \(i\) 个,有 \(j\) 个在 \((a_{fa_u},a_u]\),剩下大于 \(a_u\),即要在剩下 \(sz_u-(i-j)\) 个节点、\(a_u-a_{fa_u}\) 个高度中选择 \(j\) 个位置放点。于是有转移方程:
注意两次转移中都是小的更新大的,所以 \(i\) 要倒序遍历。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=505,mod=1e9+7,maxm=1e6+5;
int n,m,a[maxn],sz[maxn];
int rt,top,zhan[maxn];
int ls[maxn],rs[maxn];
int f[maxn][maxn];
int fac[maxm],inv[maxm];
il int qpow(int x,int y){
int res=1;
while(y){
if(y&1){
res=res*1ll*x%mod;
}
x=x*1ll*x%mod,y>>=1;
}
return res;
}
il void init(int x){
fac[0]=1;
for(int i=1;i<=x;i++){
fac[i]=fac[i-1]*1ll*i%mod;
}
inv[x]=qpow(fac[x],mod-2);
for(int i=x;i;i--){
inv[i-1]=inv[i]*1ll*i%mod;
}
}
il int C(int x,int y){
if(x<y||y<0){
return 0;
}
return fac[x]*1ll*inv[y]%mod*inv[x-y]%mod;
}
il int A(int x,int y){
if(x<y||y<0){
return 0;
}
return fac[x]*1ll*inv[x-y]%mod;
}
il void dfs(int u,int fa){
if(!u){
return ;
}
dfs(ls[u],u),dfs(rs[u],u);
sz[u]=sz[ls[u]]+sz[rs[u]]+1;
f[u][0]=1;
for(int i=min(m,sz[u]);i;i--){
for(int j=1;j<=min(i,sz[ls[u]]);j++){
f[u][i]=(f[u][i]+f[u][i-j]*1ll*f[ls[u]][j])%mod;
}
}
for(int i=min(m,sz[u]);i;i--){
for(int j=1;j<=min(i,sz[rs[u]]);j++){
f[u][i]=(f[u][i]+f[u][i-j]*1ll*f[rs[u]][j])%mod;
}
}
for(int i=min(m,sz[u]);i;i--){
for(int j=1;j<=i;j++){
f[u][i]=(f[u][i]+f[u][i-j]*1ll*C(a[u]-a[fa],j)%mod*A(sz[u]-i+j,j))%mod;
}
}
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
read(n)read(m);
for(int i=1;i<=n;i++){
read(a[i]);
while(top&&a[zhan[top]]>a[i]){
top--;
}
int &tmp=top?rs[zhan[top]]:rt;
ls[i]=tmp;
tmp=zhan[++top]=i;
}
init(1e6);
dfs(rt,0);
printf("%d",f[rt][m]);
return 0;
}
}
int main(){return asbt::main();}
[hdu4125]Moles
显然我们找到原树模 \(2\) 意义下的欧拉序,跑一个 kmp 就好了。
考虑如何建树,这棵树按 \(a\) 值满足二叉搜索树的性质,按下标满足小根堆的性质。所以这就是一棵笛卡尔树。线性建树即可。时间复杂度 \(O(n\log n)\)。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=6e5+5;
int T,n,zhan[maxn],nxt[maxn];
int a[maxn],p[maxn],tot;
int ls[maxn],rs[maxn];
char s[maxn],t[maxn<<1];
il void dfs(int u){
t[++tot]=u&1|48;
if(ls[u]){
dfs(ls[u]);
t[++tot]=u&1|48;
}
if(rs[u]){
dfs(rs[u]);
t[++tot]=u&1|48;
}
}
il void solve(){
read(n);
for(int i=1;i<=n;i++){
read(a[i]);
p[i]=i;
}
scanf(" %s",s+1);
int len=strlen(s+1);
int top=0,rt=0;
sort(p+1,p+n+1,[](const int &x,const int &y){return a[x]<a[y];});
for(int i=1;i<=n;i++){
while(top&&p[zhan[top]]>p[i]){
top--;
}
int &tmp=top?rs[zhan[top]]:rt;
ls[i]=tmp;
tmp=zhan[++top]=i;
}
tot=0;
dfs(rt);
// for(int i=1;i<=tot;i++){
// cout<<t[i];
// }
// puts("");
nxt[1]=0;
for(int i=2,j=0;i<=len;i++){
while(j&&s[j+1]!=s[i]){
j=nxt[j];
}
if(s[j+1]==s[i]){
j++;
}
nxt[i]=j;
}
int ans=0;
for(int i=1,j=0;i<=tot;i++){
while(j&&s[j+1]!=t[i]){
j=nxt[j];
}
if(s[j+1]==t[i]){
j++;
}
if(j==len){
ans++,j=nxt[j];
}
}
printf("%d\n",ans);
for(int i=1;i<=n;i++){
ls[i]=rs[i]=0;
}
}
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
// cout<<cplx::usdmem();
read(T);
for(int i=1;i<=T;i++){
printf("Case #%d: ",i);
solve();
}
return 0;
}
}
int main(){return asbt::main();}
[hdu6854]Kcats
考虑 \(a\) 的实际含义,即为 \(p\) 的小根笛卡尔树上 \(i\) 到根上权值 \(\le\) 的点数。于是可以进行区间 dp。设 \(f_{i,j,x}\) 表示 \([i,j]\) 的根的 \(a\) 值为 \(x\) 的方案数。于是有转移:
时间复杂度是 \(O(Tn^4)\) 的,但是能过。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
using namespace std;
namespace asbt{
int main(){
ios::sync_with_stdio(0),cin.tie(0);
const int mod=1e9+7;
function<int(int,int)>qpow=[](int x,int y)->int{
int res=1;
while(y){
if(y&1){
res=res*1ll*x%mod;
}
y>>=1,x=x*1ll*x%mod;
}
return res;
};
int *fac=new int[105](),*inv=new int[105]();
fac[0]=1;
for(int i=1;i<=100;i++){
fac[i]=fac[i-1]*1ll*i%mod;
}
inv[100]=qpow(fac[100],mod-2);
for(int i=100;i;i--){
inv[i-1]=inv[i]*1ll*i%mod;
}
// for(int i=0;i<=100;i++){
// cout<<fac[i]<<" "<<fac[i]*1ll*inv[i]%mod<<"\n";
// }
function<int(int,int)>C=[=](int x,int y)->int{
if(x<y||y<0){
return 0;
}
return fac[x]*1ll*inv[y]%mod*inv[x-y]%mod;
};
int *a=new int[105]();
int (*f)[105][105]=new int[105][105][105]();
int T;
cin>>T;
while(T--){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++){
f[i][j][k]=0;
}
}
}
for(int len=1;len<=n;len++){
for(int i=1,j=len;j<=n;i++,j++){
for(int k=i,l,r;k<=j;k++){
if(~a[k]){
l=r=a[k];
}
else{
l=1,r=n;
}
for(int x=l;x<=r;x++){
(f[i][j][x]+=C(j-i,k-i)*1ll*(i<k?f[i][k-1][x]:1)%mod*(k<j?f[k+1][j][x+1]:1)%mod)%=mod;
}
}
}
}
cout<<f[1][n][1]<<"\n";
}
delete[] fac,inv,a,f;
return 0;
}
}
int main(){return asbt::main();}
啊上面这个因为 hdu 评测机太慢过不了,这告诉我们可以用杨辉三角时就用吧,还有不要用 new
定义数组。。。
Code
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
using namespace std;
namespace asbt{
const int mod=1e9+7;
int C[105][105],a[105],f[105][105][105];
int main(){
ios::sync_with_stdio(0),cin.tie(0);
for(int i=0;i<=100;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
}
int T;
cin>>T;
while(T--){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++){
f[i][j][k]=0;
}
}
}
for(int len=1;len<=n;len++){
for(int i=1,j=len;j<=n;i++,j++){
for(int k=i,l,r;k<=j;k++){
if(~a[k]){
l=r=a[k];
}
else{
l=1,r=n;
}
for(int x=l;x<=r;x++){
(f[i][j][x]+=C[j-i][k-i]*1ll*(i<k?f[i][k-1][x]:1)%mod*(k<j?f[k+1][j][x+1]:1)%mod)%=mod;
}
}
}
}
cout<<f[1][n][1]<<"\n";
}
return 0;
}
}
int main(){return asbt::main();}
复杂度看起来假的离谱的 dp 如果常数小有可能就是真的了……
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步