无旋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

样例

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*/

无旋treap大法好的更多相关文章

  1. [转载]无旋treap:从单点到区间(例题 BZOJ1500&NOI2005 维护数列 )

    转自ZZH大佬,原文:http://www.cnblogs.com/LadyLex/p/7182631.html 1500: [NOI2005]维修数列 Time Limit: 10 Sec  Mem ...

  2. [转载]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)

    转载自ZZH大佬,原文:http://www.cnblogs.com/LadyLex/p/7182491.html 今天我们来学习一种新的数据结构:无旋treap.它和splay一样支持区间操作,和t ...

  3. [您有新的未分配科技点]无旋treap:从好奇到入门(例题:bzoj3224 普通平衡树)

    今天我们来学习一种新的数据结构:无旋treap.它和splay一样支持区间操作,和treap一样简单易懂,同时还支持可持久化. 无旋treap的节点定义和treap一样,都要同时满足树性质和堆性质,我 ...

  4. Luogu 3369 / BZOJ 3224 - 普通平衡树 - [无旋Treap]

    题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=3224 https://www.luogu.org/problemnew/show/P3 ...

  5. 【算法学习】Fhq-Treap(无旋Treap)

    Treap——大名鼎鼎的随机二叉查找树,以优异的性能和简单的实现在OIer们中广泛流传. 这篇blog介绍一种不需要旋转操作来维护的Treap,即无旋Treap,也称Fhq-Treap. 它的巧妙之处 ...

  6. 无旋treap的区间操作实现

    最近真的不爽...一道维修数列就做了我1上午+下午1h+1晚上+晚上1h+上午2h... 一道不错的自虐题... 由于这一片主要讲思想,代码我放这里了 不会无旋treap的童鞋可以进这里 呵呵... ...

  7. 无旋treap的简单思想以及模板

    因为学了treap,不想弃坑去学splay,终于理解了无旋treap... 好像普通treap没卵用...(再次大雾) 简单说一下思想免得以后忘记.普通treap因为带旋转操作似乎没卵用,而无旋tre ...

  8. [BZOJ3223]文艺平衡树 无旋Treap

    3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec  Memory Limit: 128 MB Description 您需要写一种数据结构(可参考题目标题),来维护一个 ...

  9. [您有新的未分配科技点] 无旋treap:从单点到区间(例题 BZOJ1500&NOI2005 维护数列 )

    1500: [NOI2005]维修数列 Time Limit: 10 Sec  Memory Limit: 64 MB Description Input 输入的第1 行包含两个数N 和M(M ≤20 ...

随机推荐

  1. Python 遍历目录下的子目录和文件

    import os A: 遍历目录下的子目录和文件 for root,dirs ,files in os.walk(path) root:要访问的路径名 dirs:遍历目录下的子目录 files:遍历 ...

  2. ng 使用阿里巴巴矢量图

    1.进入阿里巴巴矢量图标库中,选择需要下载的图标,添加进项目中 2.进去项目选择Font class 模式,然后下载到本地 3.解压下载的压缩包,把.css/.svg/.ttf/.woff/.woff ...

  3. VS删除空白行

    使用正则表达式, 搜索 (?<=\r\n)\r\n 替换空白

  4. C#工具类MySqlHelper,基于MySql.Data.MySqlClient封装

    源码: using System; using System.Collections.Generic; using System.Linq; using System.Text; using Syst ...

  5. Java基础之UDP协议和TCP协议简介及简单案例的实现

    写在前面的废话:马上要找工作了,做了一年的.net ,到要找工作了发现没几个大公司招聘.net工程师,真是坑爹呀.哎,java就java吧,咱从头开始学呗,啥也不说了,玩命撸吧,我真可怜啊. 摘要: ...

  6. C#中字符串的操作大全

    一.C#中字符串的建立过程 例如定义变量 strT="Welcome to "; strT+="www.cuit.edu.cn"; 程序首先创建一个System ...

  7. C#中几种单例模式

    1.静态代码块 /// <summary> /// 静态代码块 /// 仅在第一次调用类的任何成员时自动执行 /// </summary> public class Singl ...

  8. PIE SDK打开自定义矢量数据

    1. 数据介绍 信息提取和解译的过程中,经常会生成一部分中间临时矢量数据,这些数据在执行完对应操作后就失去了存在的价值,针对这种情况,PIE增加了内存矢量数据集,来协助用户完成对自定义矢量数据的读取和 ...

  9. Java自学-集合框架 与数组的区别

    Java集合框架与数组的区别 示例 1 : 使用数组的局限性 如果要存放多个对象,可以使用数组,但是数组有局限性 比如 声明长度是10的数组 不用的数组就浪费了 超过10的个数,又放不下 //Test ...

  10. jquery.uploadView 实现图片预览上传

    图片上传,网上有好多版本,今天也要做一个查了好多最终找到了一个uploadview 进行了一下修改 来看代码 @{ Layout = null; } <!DOCTYPE html> < ...