最详细笛卡尔树,看不懂私信骂我!

笛卡尔树的性质:

笛卡尔树有以下两种数据结构的性质

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,首先,变量 val 和 id 比较好理解,就是这个点的点权和编号。

        2,其次,大眼一看,两个儿子指针全都定义为 node 类型,指向的是整个左子树(和右子树)。因为左指针也是 node 类型,所以这个指针也包含了它自己的左右儿子、点权信息,类似于去理解朴素的链表去理解它。

        3,结构体中的这个成员函数是没有函数体的哦,因为我们只需要把已知的点权和编号赋值就可以了。并且,为了防止指针乱指,要把左右儿子指针都赋值为空。可以理解为初始化时,左右儿子并不存在。一会我们再讲如何正确赋值左右儿子。

        这个结构体内的函数什么时候调用呢?我们在创建新节点时就会自动调用。

2,构建笛卡尔树时需要用到的单调栈:

为了满足笛卡尔树的两种性质,我们考虑用 c++ 中的栈去维护节点点权,构造出笛卡尔树。

首先是定义部分。

stack <node*> st;

这个名为 st 的栈存储着指向 node 类型的指针。

如何维护栈单调?

对于每一个插入的点权,分两种情况讨论。如果插入的点权大于栈顶,直接插入即可,这时此栈仍然单调。如果插入的点权小于栈顶,一直把栈顶清空直到栈空或栈顶大于该插入节点,然后插入新节点。

插入节点时笛卡尔树的形状变化

对于第一种情况,由于要满足小根堆的性质,我们必须把新节点放到栈顶结点的子树中,我们把这个节点插入到栈顶结点的右孩子中。为什么?再看笛卡尔树的第一条性质,中序遍历得到原序列。现在我们已知了所有节点的权值,在插入时,对于每一个子树,相对于这个子树树根(先插入),后插入的一定在右儿子中(中序遍历基本顺序),明白了么?嘿嘿。

对于第二种情况,当我们不断弹出栈顶时,因为所有被弹出的栈顶都大于当前节点,因此可以把它们全部放入当前节点的左子树,为什么是左子树?因为这些节点都在当前结点之前出现,为了满足中序遍历的规则,把它们放入左子树。为什么把新入结点放入当前栈中第一个比它小的结点的右子树中呢?因为新入结点比它出现更晚。既然在这棵子树中,我们把栈中第一个比它小的结点当成根,那么中序遍历时自然要把新入结点往后放放啦!明白了吗?嗯嗯!

用来辅助理解的例子

如果还不理解,没关系啦,这里有一个由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,建树的主要步骤

                首先我们要定义一个根节点 ro ,类型为指向 node 类型的指针。

                然后遍历权值数组,对于每一个权值,我们同样要定义一个指向 node 的指针,名为当前指针 cur ,对它赋值。注意,只需要把 idval 成员赋值即可,左右儿子在我们创建时已经调用了赋值函数。

                接着,对栈进行操作,由于左儿子的指向较为复杂(需要把整个子树插入),我们先定义一个左儿子指针,类型也是指向 node 类型的指针,初始化为空。如果栈非空,我们就要不断弹出栈顶直到找到第一个小于新入结点的栈顶。所以我们不能暴力地直接清空,而是不断 pop 掉。实际上是刚才思路的简单诠释哈。

                最后我们需要对栈的情况进行判断,如果此时栈空,证明整棵树的根就是新入结点,还记得我们定义的根节点吗?把它赋值为 cur 即可。如果栈非空,我们需要把刚才 pop 掉的所有结点连到 cur 的左儿子上,怎么弄呢?难道一个一个地接吗?不是哦~( ̄▽ ̄)~*,由于在 pop 之前这棵树和栈都自成体系,刚才 pop 掉的所有栈顶肯定都已经连好了呀!仔细想想发现是不是挺有道理的呢?只需要把最后一次弹出的接上就行。

                现在我们保证了栈的单调性,树的正确性,就可以安心地把 cur 放入栈内了。看懂了后,代码也是非常好写了不是吗?

        2,完善洛谷P5854笛卡尔树的板子

                原题要求 xor^n_{i=1} i \times(l_i +1) 和 xor^n_{i=1} i\times(r_i+1)

                考虑开两个数组分别表示左儿子和右儿子的权值,然后不断递归直到左右儿子为空,也是非常好模拟哈,在打表完之后,开两个答案记录一下,最后输出即可。

        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;
}

  看不懂来私信骂学姐!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值