无旋treap大法好

无旋Treap大法好

原理?

  • 是一棵二叉查找树: 一个节点左子树权值都比他小,右子树权值都比他大
    • 所以可以维护序列(以位置为权值),或数值(以数值为权值)
  • 是一个: 每个节点除了上述提到的权值外,还有一个随机生成的给堆用的优先级
    • 好像有了随机+堆就可以使树高均摊\(O(log_2^n)\)?,怎么证明呢?

分裂合并两个操作为基础,写起来会比较清真

基本操作

结构体的组成

struct Treap
{
    LL val, pri; //数值, 优先级
    int cnt, siz, ch[2]; // 计数, 子树大小, 左右儿子
    //还可以加一些标记,如这个子树翻转的标记
    Treap() {}
    Treap(int _val) { val = _val, pri = rand(), cnt = siz = 1, ch[0] = ch[1] = 0; }
} t[MAXN];

split(按数值)

这两个分裂的代码看了很久才看懂

split(int now, int val, int & x, int & y)意思是将\(now\)为根的这棵子树, \(<=val\)的部分分裂, 并将他的根记录在\(x\)上, \(y\)同理

那么怎么递归实现呢?

主要思路是一旦碰到能整棵子树都\(或>val 或<=val\)就整棵一起分割

注意在当前子树的根\(now\)分裂的时候, 只能确保一边的整棵子树完全归属\(<=val\)\(>val\)的部分

例如当val < t[now].val, 只能确定整棵右子树及\(now\)\(>val\),所以现将\(y\)赋值为他们的根(\(now\)),

但是在左子树仍可能有\(>val\)的节点, 所以将\(now\)的左子作为参数传递, 意在后来分离出的\(>val\)的节点接在\(now\)

而现在还并不能分离\(<=val\)的某棵子树, 于是将传递过来的\(x\)继续传下去

另一种情况类似

void split(int now, int val, int & x, int & y) // <=val --> x, > val --> y
{
    if (!now) return (void)(x = y = 0);
    if (val < t[now].val) y = now, split(ls, val, x, ls);
    else x = now, split(rs, val, rs, y);
    update(now);
}

split(按大小)

void split_k(int now, int k, int & x, int & y) // 当前分割跟为now的子树, 并将<=k的根连到x(赋值给x),y同理
{
    if (!now) return (void)(x = y = 0); //不断往下分,分完了
    if (k <= t[ls].siz) y = now, split_k(ls, k, x, ls); // 确定根及右子树都>k,所以先分出他们,然后去分左子树, 将剩下的>k连回左子,<=k的连向x
    else x = now, split_k(rs, k - t[ls].siz - 1, rs, y); // 确定根及左子树<=k, 分出他们,然后分右子树,将右子树中<=k的连回右子,>k的连向y
	update(now);
}

merge

int merge(int x, int y) // max_x < min_y
{
    if (!x || !y) return x + y;
    if (t[x].pri < t[y].pri)
    {
        t[x].ch[1] = merge(t[x].ch[1], y);
        update(x);
        return x;
    }
    else 
    {
        t[y].ch[0] = merge(x, t[y].ch[0]);
        update(y);
        return y;
    }
}
LL kth(int k)
{
    int now = root;
    while (1)
    {
        if (t[rs].siz >= k) now = rs;
        else if (t[rs].siz + 1 < k) k -= t[rs].siz + 1, now = ls;
        else return t[now].val;
    }
}

例题1

BZOJ1500,NOI2005,维修数列

用这道毒瘤题具体分析一些\(Treap\)支持的操作

题意概述

维护一个数列,

要求区间插入/删除/统一修改/翻转/求和, 求当前整个数列最大子段和(至少包含一个数)

任何时刻数列中最多含有500 000个数,数列中任何一个数字均在[-1 000, 1 000]内。
插入的数字总数不超过4 000 000个,输入文件大小不超过20MBytesN 和M(M ≤20 000

233

样例

Sample Input

9 8
2 -6 3 5 1 -5 -3 6 3
GET-SUM 5 4
MAX-SUM
INSERT 8 3 -5 7 2
DELETE 12 1
MAKE-SAME 3 3 2
REVERSE 3 6
GET-SUM 5 4
MAX-SUM

Sample Output

-1
10
1
10

\(Treap\)的具体操作

\(O(n)\)建树

主要用于对数列的建树, 比一个一个插入优良一点

思路是: 对于一个新的节点, 他一定在之前节点构成的\(Treap\)的右边(废话),

所以想象一下把他从右边插入, 找到最右边那条链上第一个堆的优先级比他大的位置, 把他和他的整棵子树切下来, 把新节点换上去, 再结回新节点的左儿子

可以用栈实现

int newnode(int val)
{
	int now = renode ? recyc[renode --] : ++totnode; // 循环利用
    t[now] = Treap(val);
    return now;
}
int build(int tot) 
{
    for (int i = 1; i <= tot; ++ i)
    {
        int now = newnode(in()), rec = 0;
        while (top && t[now].pri < t[stk[top]].pri)
            update(stk[top]), rec = stk[top --];
        if (top) t[stk[top]].ch[1] = now;
        t[now].ch[0] = rec;
        stk[++top] = now;
    }
    while (top) update(stk[top --]);
    return stk[1];
}

分离出一个区间

例如将\([l, r]\)分离到\(y\)

int x = 0, y = 0, z = 0;
split(root, l - 1, x, y); // x <= l-1, y >= l
split(y, r - l + 1, y, z); // y [l, r]

insert一个点

int newnode(int val)
{
    t[++tot] = Treap(val);
    return tot;
}
void insert(int val)
{
    int x = 0, y = 0, xx = 0, xy = 0;
    split(root, val, x, y);
    root = merge(merge(x, newnode(val)), y);
}

insert一个区间

void insert(int pos, int tot) // ok
{
    int x, y;
    split(root, pos, x, y);
    root = merge(merge(x, build(tot)), y);
}

delete一个区间

void delet(int pos, int tot)
{
    int x, y, z;
    split(root, pos - 1, x, y);
    split(y, tot, y, z);
    root = merge(x, z);
	eat(y); // 回收
}

第k大

LL kth(int k)
{
    int now = root;
    while (1)
    {
        if (t[rs].siz >= k) now = rs;
        else if (t[rs].siz + 1 < k) k -= t[rs].siz + 1, now = ls;
        else return t[now].val;
    }
}

关于打标记

因为要保证update(now)时左右两个儿子的信息已经更新, 所以标记代表本节点已经修改,子树需要修改,这样比较好

下面就是两种标记

区间翻转

先分离出要翻转的区间, 打上标记

不用担心之后这个有标记的根不是恰好代表这段区间

因为之后的合并/分裂操作, 一旦需要改变某个节点\(x\)在树中的结构, 就会立马下传标记, 并且之后会向上更新

void markrev(int now) // 标记这个子树需要翻转, 意味着子树内每个点左右儿子都交换
{
	if (!now) return ;
	t[now].rev ^= 1; 
	swap(t[now].pre, t[now].suf); // 用于维护区间最大子段和, 区间交换那么前缀和后缀也要交换
	swap(ls, rs);
}
void reverse(int l, int r) 
{
    int x = 0, y = 0, z = 0;
    split(root, l - 1, x, y); // x <= l-1, y >= l
    split(y, r - l + 1, y, z); // y [l, r]
    markrev(y); // !
    root = merge(merge(x, y), z);
}

区间赋值

和翻转类似

void markcov(int now, int cov) 
{
	if (!now) return ;
	t[now].val = t[now].cov = cov;
	t[now].sum = t[now].val * t[now].siz;
	t[now].pre = t[now].suf = max(t[now].sum, 0);
	t[now].maxsum = max(t[now].sum, t[now].val);
}
void modify(int l, int r, int c) 
{
    int x, y, z;
    split(root, l - 1, x, y);
    split(y, r - l + 1, y, z);
    markcov(y, c); // !
    root = merge(merge(x, y), z);
}

区间求和

每个节点维护子树内的权值就可以了, 具体的维护下面区间修改的更新中说明

int getsum(int l, int r) 
{
    int x, y, z, ret;
    split(root, l - 1, x, y);
    split(y, r - l + 1, y, z);
    ret = t[y].sum;
    root = merge(merge(x, y), z);
    return ret;
}

\(和相关pushdown() 和 update() 相关\)

之前已经说过, 打标记时就修改此节点的信息, 而标记表示子树需要修改

单独写了加上标记的函数(结合之前几段代码)

void pushdown(int now)
{
	if (!now) return ;
    if (t[now].rev) 
    {
		markrev(ls), markrev(rs);
        t[now].rev = 0;
    }
    if (t[now].cov != 10000) //
    {
		markcov(ls, t[now].cov), markcov(rs, t[now].cov);
        t[now].cov = 10000;
    }
}

更新是确保子树信息正确

void update(int now) // ls ans rs 's info must be right
{
    t[now].siz = t[ls].siz + t[rs].siz + 1; 
    t[now].sum = t[ls].sum + t[rs].sum + t[now].val; 
    t[now].pre = max(max(t[ls].pre, t[ls].sum + t[now].val + t[rs].pre), 0);
    t[now].suf = max(max(t[rs].suf, t[rs].sum + t[now].val + t[ls].suf), 0);
    t[now].maxsum = t[now].val + t[ls].suf + t[rs].pre;
	if (ls) t[now].maxsum = max(t[now].maxsum, t[ls].maxsum);
	if (rs) t[now].maxsum = max(t[now].maxsum, t[rs].maxsum);
}

垃圾回收, 节省空间

int newnode(int val)
{
	int now = renode ? recyc[renode --] : ++totnode;
    t[now] = Treap(val);
    return now;
}
void eat(int now)
{
	if (!now) return ;
	eat(ls); eat(rs);
	recyc[++renode] = now;
}
void delet(int pos, int tot)
{
    int x, y, z;
    split(root, pos - 1, x, y);
    split(y, tot, y, z);
    root = merge(x, z);
	eat(y);
}

BZOJ1500 代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>

using namespace std;
const int MAXN = 5e5 + 10;
inline int in()
{
    int x = 0, flag = 1; char ch = getchar();
    while (ch < '0' || ch > '9') { if (ch == '-') flag = -1; ch = getchar(); } 
    while (ch >= '0' && ch <= '9') x = (x << 3) + (x << 1) + (ch ^ 48), ch = getchar();
    return x * flag;
}

int n, m;

#define ls (t[now].ch[0])
#define rs (t[now].ch[1])
int root, totnode;
int recyc[MAXN], renode;
struct Treap
{
    int val, pri, siz, ch[2], rev, cov, sum, pre, suf, maxsum;
    Treap() {}
    Treap(int _val) 
        { 
            sum = val = _val, pri = rand(), siz = 1, ch[0] = ch[1] = rev = 0; 
            maxsum = pre = suf = max(0, val);
            cov = 1e4;
        }
} t[MAXN];
void markrev(int now) //ok
{
	if (!now) return ;
	t[now].rev ^= 1;
	swap(t[now].pre, t[now].suf);
	swap(ls, rs);
}
void markcov(int now, int cov) // ok
{
	if (!now) return ;
	t[now].val = t[now].cov = cov;
	t[now].sum = t[now].val * t[now].siz;
	t[now].pre = t[now].suf = max(t[now].sum, 0);
	t[now].maxsum = max(t[now].sum, t[now].val);
}
void pushdown(int now)
{
	if (!now) return ;
    if (t[now].rev) 
    {
		markrev(ls), markrev(rs);
        t[now].rev = 0;
    }
    if (t[now].cov != 10000) //
    {
		markcov(ls, t[now].cov), markcov(rs, t[now].cov);
        t[now].cov = 10000;
    }
}
void update(int now) // ls ans rs 's info must be right
{
    t[now].siz = t[ls].siz + t[rs].siz + 1; // ok
    t[now].sum = t[ls].sum + t[rs].sum + t[now].val; // ok
    t[now].pre = max(max(t[ls].pre, t[ls].sum + t[now].val + t[rs].pre), 0);
    t[now].suf = max(max(t[rs].suf, t[rs].sum + t[now].val + t[ls].suf), 0);
    t[now].maxsum = t[now].val + t[ls].suf + t[rs].pre;
	if (ls) t[now].maxsum = max(t[now].maxsum, t[ls].maxsum);
	if (rs) t[now].maxsum = max(t[now].maxsum, t[rs].maxsum);
//pay attention
}
void split(int now, int k, int & x, int & y) 
{ 
    if (!now) return (void)(x = y = 0); 
    pushdown(now);
    if (k <= t[ls].siz) y = now, split(ls, k, x, ls); 
    else x = now, split(rs, k - t[ls].siz - 1, rs, y); 
	update(now);
}
int merge(int x, int y) // max_x < min_y
{
	pushdown(x); pushdown(y);
    if (!x || !y) return x + y;
    if (t[x].pri < t[y].pri)
    {
        t[x].ch[1] = merge(t[x].ch[1], y);
        update(x);
        return x;
    }
    else 
    {
        t[y].ch[0] = merge(x, t[y].ch[0]);
        update(y);
        return y;
    }
}
int newnode(int val)
{
	int now = renode ? recyc[renode --] : ++totnode;
    t[now] = Treap(val);
    return now;
}

int stk[MAXN], top;
int build(int tot) // ok
{
    for (int i = 1; i <= tot; ++ i)
    {
        int now = newnode(in()), rec = 0;
        while (top && t[now].pri < t[stk[top]].pri)
            update(stk[top]), rec = stk[top --];
        if (top) t[stk[top]].ch[1] = now;
        t[now].ch[0] = rec;
        stk[++top] = now;
    }
    while (top) update(stk[top --]);
    return stk[1];
}
void insert(int pos, int tot) // ok
{
    int x, y;
    split(root, pos, x, y);
    root = merge(merge(x, build(tot)), y);
}
void eat(int now)
{
	if (!now) return ;
	eat(ls); eat(rs);
	recyc[++renode] = now;
}
void delet(int pos, int tot)
{
    int x, y, z;
    split(root, pos - 1, x, y);
    split(y, tot, y, z);
    root = merge(x, z);
	eat(y);
}
int getsum(int l, int r) // ok
{
    int x, y, z, ret;
    split(root, l - 1, x, y);
    split(y, r - l + 1, y, z);
    ret = t[y].sum;
    root = merge(merge(x, y), z);
    return ret;
}
int maxsum() //ok
{
    return t[root].maxsum;
}
void modify(int l, int r, int c) // ok
{
    int x, y, z;
    split(root, l - 1, x, y);
    split(y, r - l + 1, y, z);
    markcov(y, c);
    root = merge(merge(x, y), z);
}
void reverse(int l, int r) // ok
{
    int x = 0, y = 0, z = 0;
    split(root, l - 1, x, y); // x <= l-1, y >= l
    split(y, r - l + 1, y, z); // y [l, r]
    markrev(y);
    root = merge(merge(x, y), z);
}
#undef ls
#undef rs

char opt[15];

int main()
{
    n = in(), m = in();
    root = build(n);
    int pos, tot;
    while (m --)
    {
        scanf("%s", opt + 1);
        if (opt[3] == 'X') // MAX-SUM 当前最子段和
            printf("%d\n", maxsum());
        else 
        {
            pos = in(), tot = in();
            if (opt[1] == 'G') // GET-SUM 从pos开始tot个数的和
                printf("%d\n", getsum(pos, pos + tot - 1));
            else if (opt[1] == 'I') // INSERT 在pos之后插入a[1~tot]
                insert(pos, tot);
            else if (opt[1] == 'D') // DELETE 删除从pos开始tot位
                delet(pos, tot);
            else if (opt[3] == 'K') // MAKE-SAME 将pos开始tot位修改为c
                modify(pos, pos + tot - 1, in());
            else if (opt[1] == 'R') // REVERSE 翻转从pos开始tot位
                reverse(pos, pos + tot - 1);
        }
    }
    return 0;
}
/*201907122010 ~ 201907131247*/
posted @ 2019-07-12 20:36  Kuonji  阅读(238)  评论(0编辑  收藏  举报
nmdp