暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

算法竞赛系列 | 笛卡儿树

348

数据结构是数据的组织形式和访问方法,前文介绍了基础数据结构,在基础上发展出了很多高级数据结构,它们原理复杂,编程困难。

为什么需要这么多高级数据结构?这是在特定应用背景下高效处理数据的需要。基础数据结构不够强大,像数组、栈、队列这样的线性结构,计算复杂度为O(n),在面对大量数据时力不从心;二叉树、堆、哈希很高效,但是它们过于简单,在处理很多特定问题时操作不便。高级数据结构与某些应用背景紧密结合,能高效地解决问题。例如,集合问题用并查集;区间问题用树状数组、线段树、分块、莫队算法;混合问题用块状链表;动态查询用平衡树,等等。

本节详细介绍笛卡儿树的概念、实现方法、应用。


01

笛卡儿树的概念


Treap树的每个节点有两个属性:键值、随机的优先级。笛卡儿树(Cartesian Tree笛卡儿树(Cartesian Tree)是一种特殊的、简化的Treap树,它的每个节点的键值预先给定,但是优先级或者预先给定,或者随机生成。

笛卡儿树主要用于处理一个确定的数列。数列中的一个数有两个属性:在数列中的位置、数值。把位置看作键值,数值看作优先级,根据这个数列构造出来的笛卡儿树符合Treap树的两个特征如下。

(1) 以每个数的位置为键值,它是一棵BST。也就是说,对这棵树做中序遍历,返回的就是数列。

(2) 把数值看作Treap树的优先级,把数列建成一个堆。如果按大根堆建这棵树,那么在每个子树上,子树的根的权值是整棵子树上最大的。

图4.63给出一个用数列{2,7,6,24,8,15,5,11,19}建笛卡儿树的例子。

■ 图4.63用笛卡儿树存储数列


02

用单调栈建笛卡儿树


前面介绍的Treap树,若给定了每个节点的键值和优先级,那么Treap树的形态是唯一的。与之类似,笛卡儿树的形态也是唯一的。如何把数列建成一棵笛卡儿树?如果用前面Treap建树的做法,逐一加入节点,然后根据临时分配的优先级用旋转、分裂等操作调整形态,复杂度为O(log2n)

笛卡儿树比普通Treap树简单,建树也更简单,有O(n)复杂度的建树方法。例如图4.64的例子,数列有5个数,从图4.64(a)到图4.64(d),逐一插入树上。

■ 图4.64借助单调栈建笛卡儿树


每次插入新的节点,横向的位置是固定的,新的点总是插入最右边,而且需要调整的是纵向位置,所以只需要考虑“最右链”,即从根开始一直沿着右儿子向下走的那条链。在最右链上,从上一个插入的点开始,按最大堆的要求寻找合适的纵向位置。新插入的节点,在插入结束之后,一定位于最右链的末端。

这个过程的实现用单调栈很容易操作。以大根堆为例,用栈维护从根开始的最右链,栈底最大,栈顶最小。例如,图4.64(c)中栈为{11,15,24},它是树上的右链,栈底是根24,栈顶是右链的末端11。

下面解释建树的步骤,从左到右把数字逐个插入树,并与栈中存储的右链节点比较。

(1) 插入第1个点6,6入栈。

(2) 插入第2个点24。24比栈顶的6(位于当前的右链上)大,弹出6,24入栈。6是24的左儿子。

(3) 插入第3个点15,15比栈顶24小,15入栈,15插入24的右子树;插入第4个点11,11比栈顶的15小,11入栈,11插入15的右子树。

(4) 插入第5个点19,19比栈顶11和15大,弹出11和15,然后19入栈,15是19的左儿子,19是24的右儿子。后面没有新的节点,结束。

在建树过程中,每个点入栈一次,出栈一次或不出栈,所以总复杂度为O(n),是最快的建树方法。

下面给出一道建树的例题。


● 例4.45Binary search heap construction(poj 1785)

问题描述: 建一棵笛卡儿树。给出n个点,每个点包括两个数据:标签label、优先级pri。把这n个点建成一棵笛卡儿树,并打印树。

输入: 每行是一个测试,输入的第1个数是节点数量n,后面有n个点,每个点包括标签和优先级。

输出: 打印树,每个节点的打印格式为(<left sub-treap><label>/<priority><right sub-treap>),中序打印。

输入样例:

7 a/3 b/6 c/4 d/7 e/2 f/5 g/1

输出样例:

(((a/3)b/6(c/4))d/7((e/2)f/5(g/1)))


下面给出了两种代码:buildtree()函数没有用栈,直接查最右链;buildtree2()函数用栈处理最右链。

第55行代码对标签(Treap树的键值)先排序,非常重要。排序之后,所有节点按从左到右的顺序插入树,插入时只需要在buildtree()函数中根据优先级调整。从这里也能认识到,笛卡儿树需要预先给出所有节点的键值,而优先级则不必,在建树时临时分配优先级也可以。当然,本题的优先级pri就是数列的数字本身,不需要临时分配。

    //改写自:https://blog.csdn.net/qq_40679299/article/details/80395824
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include <stack>
    using namespace std;
    const int N = 50005;
    const int INF = 0x7fffffff;
    struct Node{
    char s[100]; int ls,rs,fa,pri;
    friend bool operator<(const Node& a,const Node& b){
    return strcmp(a.s,b.s)<0;}
    }t[N];
    void buildtree(int n){ //不用栈,直接查最右链
    for(int i=1;i<=n;i++){
    int pos = i-1; //从前一个结点开始比较,前一个结点在最右链的末端
    while(t[pos].pri < t[i].pri)
    pos = t[pos].fa; //沿着最右链一直找,直到pos的优先级比i大
    t[i].ls = t[pos].rs; //图(4):i是19,pos是24, 15调整为19的左儿子
    t[t[i].ls].fa = i; //图(4):15的父亲是19
    t[pos].rs = i; //图(4):24的右儿子是19
    t[i].fa = pos; //图(4):19的父亲是24
    }
    }
    void buildtree2(int n){ //用栈来辅助建树
    stack <int> ST; ST.push(0); //t[0]进栈,它一直在栈底
    for(int i=1;i<=n;i++){
    int pos = ST.top();
    while(!ST.empty() && t[pos].pri < t[i].pri){
    pos = t[ST.top()].fa;
    ST.pop(); //把比i优先级小的弹出栈
    }
    t[i].ls = t[pos].rs;
    t[t[i].ls].fa = i;
    t[pos].rs = i;
    t[i].fa = pos;
    ST.push(i); //每个结点都一定要进栈
    }
    }
    void inorder(int x){ //中序遍历打印
    if(x==0) return;
    printf("(");
    inorder(t[x].ls); printf("%s/%d",t[x].s,t[x].pri); inorder(t[x].rs);
    printf(")");
    }
    int main(){
    int n;
    while(scanf("%d",&n),n){
    for(int i=1;i<=n;i++){
    t[i].ls = t[i].rs = t[i].fa = 0; //有多组测试,每次要清零
    scanf(" %[^/]/%d", t[i].s, &t[i].pri); //注意输入的写法
    }
    t[0].ls = t[0].rs = t[0].fa = 0; //t[0]不用,从t[1]开始插结点
    t[0].pri = INF; //t[0]的优先级无穷大
    sort(t+1,t+1+n); //对标签先排序,非常关键。这样建树时就只需要考虑优先级pri
    buildtree(n); //buildtree2(n); 两种建树方法
    inorder(t[0].rs); //t[0]在树的最左端,第一个点是t[0].rs
    printf("\n");
    }
    return 0;
    }


    03

    笛卡儿树和RMQ问题


    笛卡儿树的最直接应用是RMQ问题。“笛卡儿树+LCA”也是一种求RMQ的方法。

    建好笛卡儿树后,若查询区间[L,R]的RMQ,首先在树上找到L和R,L和R的LCA即为区间[L,R]的RMQ。建笛卡儿树的复杂度为O(n),在树上查找L和R复杂度约为O(log2n),然后用倍增法或Tarjan算法求LCA,总复杂度与ST算法、线段树差不多。不过,在笛卡儿树上查找L和R可能很耗时,因为笛卡儿树并不一定是一棵平衡的BST。



    《算法竞赛》系列推文正在连载中,欢迎持续关注!





    扫码优惠购书


    文章转载自清华计算机学堂,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

    评论