笛卡尔树的性质:
笛卡尔树有以下两种数据结构的性质
1,二叉搜索树的性质:
对笛卡尔树进行中序遍历可以得到原序列。
中序遍历就是左子树+根+右子树。
2,小根堆的性质:
任意一棵子树(包括自身)的根节点点权小于子树上所有节点点权。
需要用到的数据结构:
1,存储笛卡尔树要用到的数据结构:
struct node{
int id;
int val;
node *left, *right;
node (int idx ,int vx) : id(idx) ,val(vx) ,left(nullptr) ,right(nullptr){}
}
接下来我来逐行解释一下这个结构体。
1,首先,变量 和
比较好理解,就是这个点的点权和编号。
2,其次,大眼一看,两个儿子指针全都定义为 类型,指向的是整个左子树(和右子树)。因为左指针也是
类型,所以这个指针也包含了它自己的左右儿子、点权信息,类似于去理解朴素的链表去理解它。
3,结构体中的这个成员函数是没有函数体的哦,因为我们只需要把已知的点权和编号赋值就可以了。并且,为了防止指针乱指,要把左右儿子指针都赋值为空。可以理解为初始化时,左右儿子并不存在。一会我们再讲如何正确赋值左右儿子。
这个结构体内的函数什么时候调用呢?我们在创建新节点时就会自动调用。
2,构建笛卡尔树时需要用到的单调栈:
为了满足笛卡尔树的两种性质,我们考虑用 c++ 中的栈去维护节点点权,构造出笛卡尔树。
首先是定义部分。
stack <node*> st;
这个名为 的栈存储着指向
类型的指针。
如何维护栈单调?
对于每一个插入的点权,分两种情况讨论。如果插入的点权大于栈顶,直接插入即可,这时此栈仍然单调。如果插入的点权小于栈顶,一直把栈顶清空直到栈空或栈顶大于该插入节点,然后插入新节点。
插入节点时笛卡尔树的形状变化
对于第一种情况,由于要满足小根堆的性质,我们必须把新节点放到栈顶结点的子树中,我们把这个节点插入到栈顶结点的右孩子中。为什么?再看笛卡尔树的第一条性质,中序遍历得到原序列。现在我们已知了所有节点的权值,在插入时,对于每一个子树,相对于这个子树树根(先插入),后插入的一定在右儿子中(中序遍历基本顺序),明白了么?嘿嘿。
对于第二种情况,当我们不断弹出栈顶时,因为所有被弹出的栈顶都大于当前节点,因此可以把它们全部放入当前节点的左子树,为什么是左子树?因为这些节点都在当前结点之前出现,为了满足中序遍历的规则,把它们放入左子树。为什么把新入结点放入当前栈中第一个比它小的结点的右子树中呢?因为新入结点比它出现更晚。既然在这棵子树中,我们把栈中第一个比它小的结点当成根,那么中序遍历时自然要把新入结点往后放放啦!明白了吗?嗯嗯!
用来辅助理解的例子
如果还不理解,没关系啦,这里有一个由DeepSeek - R1给出的简单例子。
首先是给出的结点权值数组:{3,2,6,1,9}
1,对于3,把3压入栈内。
栈情况:{ 3 }
树情况:

2,对于2,一直清空栈顶发现空了,把2压入栈内。
栈情况:{ 2 }
树情况:
3,对于6,把6压入栈内。
栈情况:{ 2,6 }
树情况:

4,对于1,一直清空栈顶发现空了,把1压入栈内。
栈情况:{ 1 }
树情况:

5,对于9,压入9。
栈情况:{ 1,9 }
树情况:

怎么样?发现了吗?任何一个时间下的树都是笛卡尔树,任何一个时间下的栈都是单调栈!
代码这一块
1,建树的主要步骤
首先我们要定义一个根节点 ,类型为指向
类型的指针。
然后遍历权值数组,对于每一个权值,我们同样要定义一个指向 的指针,名为当前指针
,对它赋值。注意,只需要把
和
成员赋值即可,左右儿子在我们创建时已经调用了赋值函数。
接着,对栈进行操作,由于左儿子的指向较为复杂(需要把整个子树插入),我们先定义一个左儿子指针,类型也是指向 类型的指针,初始化为空。如果栈非空,我们就要不断弹出栈顶直到找到第一个小于新入结点的栈顶。所以我们不能暴力地直接清空,而是不断
掉。实际上是刚才思路的简单诠释哈。
最后我们需要对栈的情况进行判断,如果此时栈空,证明整棵树的根就是新入结点,还记得我们定义的根节点吗?把它赋值为 即可。如果栈非空,我们需要把刚才
掉的所有结点连到
的左儿子上,怎么弄呢?难道一个一个地接吗?不是哦~( ̄▽ ̄)~*,由于在
之前这棵树和栈都自成体系,刚才
掉的所有栈顶肯定都已经连好了呀!仔细想想发现是不是挺有道理的呢?只需要把最后一次弹出的接上就行。
现在我们保证了栈的单调性,树的正确性,就可以安心地把 放入栈内了。看懂了后,代码也是非常好写了不是吗?
2,完善洛谷P5854笛卡尔树的板子
原题要求 和
。
考虑开两个数组分别表示左儿子和右儿子的权值,然后不断递归直到左右儿子为空,也是非常好模拟哈,在打表完之后,开两个答案记录一下,最后输出即可。
3,贴P5854的代码,不能照搬哦
#include<iostream>
#include<stack>
#define MAXN 10000005
using namespace std;
int n;
int nums[MAXN];
int l[MAXN],r[MAXN];
struct node{
int id;
int val;
node*left,*right;
node(int idx,int valx): id(idx),val(valx),left(nullptr),right(nullptr) {}
};
void bu(node*ro){
if(!ro)return;
l[ro->id]=ro->left?ro->left->id:0;
r[ro->id]=ro->right?ro->right->id:0;
bu(ro->left);
bu(ro->right);
}
void build(){
stack<node*>st;
node*ro=nullptr;
for(int i=1;i<=n;i++){
node*cur=new node(i,nums[i]);
node*last_pop=nullptr;
while(!st.empty()&&st.top()->val>cur->val) {
last_pop=st.top();
st.pop();
}
cur->left=last_pop;
if(st.empty()){
ro=cur;
}
else {
st.top()->right=cur;
}
st.push(cur);
}
bu(ro);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>nums[i];
}
build();
long long ans1=0,ans2=0;
for(int i=1;i<=n;i++){
ans1^=1LL*i*(l[i]+1);
ans2^=1LL*i*(r[i]+1);
}
cout<<ans1<<" "<<ans2<<endl;
return 0;
}
发现问题了吗?超时了一个点 QwQ,为啥呢?原因是笛卡尔树有一定概率会是一条链,或深度极深,导致栈空间爆炸或递归时被卡住。
那怎么办呢?我们可以借鉴上面的思想,但是在对栈进行操作时,直接把两个数组打好,免去了递归,也免去了繁琐的链式存储的树。相信看到这里的同学们会暗暗骂学姐一句,讲了半天还是过不了题。其实链式存储树这样的思维直接,虽然代码长,却也是练手的好题目,刚入门数据结构时,本学姐的老师也是这样要求学姐的。
下面直接贴上顺利通过的代码,上面的建树函数只要理解,下面的代码再去理解不是问题!
#include<iostream>
#define MAXN 10000005
using namespace std;
int n;
int nums[MAXN];
int l[MAXN],r[MAXN];
int st[MAXN];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>nums[i];
}
int top=0;//栈的指针,表示栈的大小
for(int i=1;i<=n;i++){
int last_pop=0;
while(top>0&&nums[st[top]]>nums[i]){
last_pop=st[top];
top--;
}
if(top>0){
r[st[top]]=i;
}
if(last_pop){
l[i]=last_pop;
}
st[++top]=i;
}
long long ans1=0,ans2=0;
for(int i=1;i<=n;i++){
ans1^=1LL*i*(l[i]+1);
ans2^=1LL*i*(r[i]+1);
}
cout<<ans1<<" "<<ans2<<endl;
return 0;
}
看不懂来私信骂学姐!

1704

被折叠的 条评论
为什么被折叠?



