【序列问题】

序列问题,一般是让你维护一个序列,支持某些操作,操作中多涉及区间。 一般解决序列问题的可选数据结构 : Splay, 非旋转式Treap, 块状链表

一些序列问题的栗子:

  • BZOJ 3223 文艺平衡树
  • CodeVs 4655 序列终结者
  • BZOJ 1507 文本编辑器 [NOI 2003]
  • POJ 3580 SuperMemo
  • Tyvj 1742 维护序列 [NOI 2005]

常见的序列操作: 给定一个长度为N的序列,要求支持

  • 插入 insert(x, k) 将x插入到k位置(也可是插入区间)
  • 删除 erase(k) 删除k位置上的元素(也可是删除区间)
  • 区间翻转 reverse(l, r) 将[l, r]区间翻转
  • 区间修改 change(l, r, delta) 将[l, r]上的元素全部加上delta(或者全部置为delta之类)
  • 区间询问 query(l, r) 询问[l, r]上的sum, min, max, 最大子段和 之类
  • etc..

【关于Treap】

Treap是一种平衡树,基于随机化和堆思想,(Treap = Tree + Heap),小巧玲珑,简单易用,在维护有序集合以及维护序列中表现不俗,深受OIer欢迎。

Treap是一颗二叉树,其上节点至少维护两个域 : value 和 priority。value是我们结点上的值(或是满足BST性质, 或是中序遍历得到原序列),priority满足堆性质,决定了树结构。priority是随机分配的,决定了我们的期望树高 $h = O(logn)$,从而得到优秀的时间复杂度。

保持Treap堆性质,一般来讲有两种实现形式,一是旋转式,二是非旋转式。

旋转式Treap基于一般平衡树的rotate()操作,写起来比较繁琐,也难以支持区间操作,在此不再介绍。

而非旋转式Treap基于merge()与split()两个基本操作,编程复杂度低,可以支持区间操作,而且由于不需要旋转,可以持久化(尽管蒟蒻的我还没弄明白可持久化Treap...),优点多多。

本文主要讨论使用非旋转式Treap解决序列问题,下文所提到的Treap,均指非旋转式Treap。


【结构体定义】

说明: 为简洁,略去了模板相关内容(Item) 全程 结构体+指针,请做好准备(^_^) 本文采用大根堆 我们在树上说到区间时,都是指其中序遍历的子区间 本文采取左闭右开区间,即[l, r)

#define L 0
#define R 1

struct Node{
    Item value; 
    int priority; // 其实可以不用这么长的名,什么weight,fix,key之类的随你便
    int size; // 以当前结点为根的子树大小,即结点数
    Node *child[2]; // 配合L、R访问左右子树
    
};
struct Treap{
    Node *root; // 这是把整棵树封装起来了,也可不封装,定义全局变量root
    Node *left, *mid, *right; // 方便下文提取区间
};

【预备工作】

为方便Treap的操作,我们先来为Node写几个小方法,如果你觉得暂时没什么用,可以先略过,等回头用到再回来看。

Node(const Item &x){
    value = x;
    priority = rand();
    size = 1;
    child[L] = child[R] = NULL;
}

~Node(){
    if(child[L]) delete child[L];
    if(child[R]) delete child[R];
} // 递归释放内存(如果采用动态内存的话)

inline int lsize(){
    return child[L] ? child[L]->size : 0;
}
inline int rsize(){
    return child[R] ? child[R]->size : 0;
} 
// 这样求size可以防止对空指针的访问,避免RE啦

inline void resize(){
    size = lsize() + rsize() + 1;
} // 重新计算子树大小

【merge】

Node* Treap::merge(Node *a, Node *b); 
// 合并a,b子树并返回结果,合并后a中元素严格在左

考虑如何合并两颗子树,我们首先自然要选定一个根 如果a的优先级比较高,我们就把a作为根,保留左子树,合并右子树和b作为新的右子树(递归); 反之同理,就这么简单的啦。

Code:

Node* merge(Node *a, Node *b){
    if(!a) return b;
    if(!b) return a;

    if(a->priority > b->priority){
        a->child[R] = merge(a->child[R], b);
        a->resize();
        return a;
    }
    else{
        b->child[L] = merge(a, b->child[L]);
        b->resize();
        return b;
    }
};

【split】

#define TreapCouple std::pair<Node*, Node*> // 用于返回两颗子树
TreapCouple Node::split(int k); 
// 将当前子树拆做[0, k)和[k, size)两颗子树

怎么拆一棵树呢,如果要拆的位置在左子树,那就拆了左子树,把拆得的右半部分作为当前树的左子树,返回拆得的左半部分和当前树。 如果在右子树同理。 同样是递归,也很简单的啦,文字可能有点绕,看Code吧:

TreapCouple split(int k){
    if(!this) return TreapCouple(NULL, NULL);
    
    TreapCouple couple;
    if(lsize() >= k){
        couple = child[L]->split(k);
        child[L] = couple.second;
        couple.second = this;
    }
    else{
        couple = child[R]->split(k - lsize() - 1);
        child[R] = couple.first;
        couple.first = this;
    }

    resize();

    return couple;
}

有了merge和split两个基本操作,我们的其他操作也就很容易实现了。


【提取区间】

提取区间,就是将树拆成三段,可以用split()方便的实现。

inline void cut(int l, int r){
    // [0, l), [l, r), [r, size)
    TreapCouple couple = root->split(l);
    left = couple.first;
    couple = couple.second->split(r - l);
    mid = couple.first;
    right = couple.second;
}

用完了还要合并回去。

inline void merge(){
    root = merge(merge(left, mid), right);
}

【build】

给定序列,如何构建出Treap?显然我们可以逐个插入,但我们有着时间复杂度更低的算法。 用栈维护整棵树最右边的一条连,即 根节点,根节点的右儿子,根节点的右儿子的右儿子... 构成的一条链,显然这条链上节点的priority值是单调的,在添加新节点的时候,我们由下往上找,一边找一边退栈,直到找到一个priority关系合适的位置,设好父子指针,将其插入进去并压入栈中。

Code:

inline Node* build(const Item data[], int n){
    std::stack<Node*> s;
    Node *node, *last;
    for(int i = 0; i < n; i++){
        node = new Node(data[i]);
        last = NULL;
        while(!s.empty() && node->fix < s.top()->fix){
            last = s.top(), s.pop();
            last->resize();
        }
        if(!s.empty()) s.top()->child[R] = node;
        node->child[L] = last;
        s.push(node);
    }
    while(s.size() != 1) s.top()->resize(), s.pop();
    return s.top()->resize(), s.top();
}

由于每个元素至多会退栈一次,故时间复杂度是$O(n)$的。


【insert】

用build()构造出要插入的区间(如果只是插入单个元素新建节点就好),然后合适位置拆分,合并。

Code:

inline void insert(int k, const Item data[], int n){
    Node *node = build(data, n);
    TreapCouple couple = root->split(k);
    root = merge(merge(couple.first, node), couple.second);
}

【erase】

提取出要删除的区间,然后把两边merge()起来就好。

Code:

inline void erase(int l, int r){
    cut(l, r);
    delete mid;
    root = merge(left, right);
}

以上就是Treap的基本操作,关于树上标记及树上维护信息的相对"高级"的问题,留到Treap(二)再谈啦。


【例题】

我们可以用这些基本操作来水一道题目~ 那就是Editor2003~ 题目链接:BZOJ 1507 只是Treap最基本的操作,水过就好啦

【AC代码】:

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <utility>
#include <stack>

#define MAXL 2597261

#define L 0
#define R 1
#define TreapCouple std::pair<Node*, Node*>

template <typename Item> struct Treap{
    struct Node{
        Item value;
        int fix;
        int size;
        Node *child[2];

        Node(const Item &x){
            value = x;
            fix = rand();
            size = 1;
            child[L] = child[R] = NULL;
        }

        ~Node(){
            if(child[L]) delete child[L];
            if(child[R]) delete child[R];
        }

        inline int lsize(){
            return child[L] ? child[L]->size : 0;
        }
        inline int rsize(){
            return child[R] ? child[R]->size : 0;
        } 

        inline void resize(){
            size = lsize() + rsize() + 1;
        }

        TreapCouple split(int k){
            // [0, k), [k, size)
            if(!this) return TreapCouple(NULL, NULL);
            TreapCouple couple;
            if(lsize() >= k){
                couple = child[L]->split(k);
                child[L] = couple.second;
                couple.second = this;
            }
            else{
                couple = child[R]->split(k - lsize() - 1);
                child[R] = couple.first;
                couple.first = this;
            }

            resize();

            return couple;
        }

        inline void print(){
            if(child[L]) child[L]->print();
            printf("%c", value);
            if(child[R]) child[R]->print();
        }
    };

    Node *root;

    Node *left, *mid, *right;

    Treap(){
        root = NULL;
        srand(19991205);
    }

    ~Treap(){
        if(root) delete root;
    }

    Node* merge(Node *a, Node *b){
        if(!a) return b;
        if(!b) return a;

        if(a->fix < b->fix){
            a->child[R] = merge(a->child[R], b);
            a->resize();
            return a;
        }
        else{
            b->child[L] = merge(a, b->child[L]);
            b->resize();
            return b;
        }
    }

    inline void cut(int l, int r){
        // [0, l), [l, r), [r, size)
        TreapCouple couple = root->split(l);
        left = couple.first;
        couple = couple.second->split(r - l);
        mid = couple.first;
        right = couple.second;
    }
    
    inline void merge(){
        root = merge(merge(left, mid), right);
    }

    inline Node* build(const Item data[], int n){
        std::stack<Node*> s;
        Node *node, *last;
        for(int i = 0; i < n; i++){
            node = new Node(data[i]);
            last = NULL;
            while(!s.empty() && node->fix < s.top()->fix){
                last = s.top(), s.pop();
                last->resize();
            }
            if(!s.empty()) s.top()->child[R] = node;
            node->child[L] = last;
            s.push(node);
        }
        while(s.size() != 1) s.top()->resize(), s.pop();
        return s.top()->resize(), s.top();
    }

    inline void insert(int k, const Item data[], int n){
        Node *node = build(data, n);
        TreapCouple couple = root->split(k);
        root = merge(merge(couple.first, node), couple.second);
    }

    inline void erase(int l, int r){
        cut(l, r);
        delete mid;
        root = merge(left, right);
    }

    inline void print(int l, int r){
        // [l, r)
        cut(l, r);
        mid->print();
        merge();
    }
};

struct Editor{
    int cursor;
    Treap<char> *text;

    Editor(){
        cursor = 0;
        text = new Treap<char>;
    }

    ~Editor(){
        delete text;
    }

    inline void insert(char str[], int n){
        text->insert(cursor, str, n);
    }

    inline void move(int pos){
        cursor = pos;
    }

    inline void prev(){
        cursor--;
    }

    inline void next(){
        cursor++;
    }

    inline void erase(int n){
        text->erase(cursor, cursor + n);
    }

    inline void print(int n){
        text->print(cursor, cursor + n);
        putchar('\n');
    }
};

char str[MAXL];
char command[10];

int main(){
    Editor *emacs = new Editor();
    int t, n;
    scanf("%d", &t);
    for(int i = 0; i < t; i++){
        scanf("%s", command);
        switch(command[0]){
            case 'I':
                scanf("%d", &n);

                for(int i = 0; i < n; i++){
                    str[i] = getchar();
                    if(str[i] == '\n') i--;
                }
                str[n] = '\0';

                emacs->insert(str, n);
                break;
            case 'M':
                scanf("%d", &n);
                emacs->move(n);
                break;
            case 'D':
                scanf("%d", &n);
                emacs->erase(n);
                break;
            case 'G':
                scanf("%d", &n);
                emacs->print(n);
                break;
            case 'P':
                emacs->prev();
                break;
            case 'N':
                emacs->next();
                break;
            default:
               throw;
        }
    }

    delete emacs;
    return 0;
}

~~我不是要黑emacs, 我不是要黑emacs, 我真的不是要黑emacs~~

就是这样啦