\(fhq-treap\)是个好东西啊!无旋转\(treap\)果然是好写,而且还是比较好理解的。

这种数据结构是由神犇fhq发明的。\(Think\ functional!\)

fhq神犇说,函数式编程的一大特点就是,不修改,只定义。普通的平衡树,无论是treap还是splay,都需要进行旋转来维护树的平衡性。

fhq神犇是通过可持久化线段树引入的。主席树其实就是一种函数式编程的产物。主席树中没有修改,只是每次新建了一条链(或者说新定义了一条链),而因为没有修改的优良性质,我们就可以使之与前面共用很大一部分,从而完成可持久化。

但是平衡树因为旋转操作需要记录父亲,所以它难以进行可持久化。毕竟,我们在rotate的过程中可是要大量修改节点之间的关系的。

需要记录父亲的数据结构一般都比较难可持久化。\(——RabbitHu\)

于是fhq神犇说,你为什么非要旋转呢?

下面就是\(fhq-treap\)最核心的两个操作——\(merge\)和\(split\)。

1、\(merge\)操作

\(merge\)就是把两棵平衡树合并成一棵。如何保持平衡呢?我们像普通的treap一样因为有随机的优先级,我们可以以这个为依据来判断如何合并。

对于\(merge(x,y)\),我们合并的前提是以x为根的树A中节点权值小于以y为根的树B中节点权值。

对于\(merge(x,y)\),如果x的优先级小于y,那么我们为了同时维护堆和二叉搜索树的性质,那么就要使y成为x的儿子,而且还需要在右子树。

反之,我们就要使x成为y的儿子,而且还要在左子树。

之后我们继续递归下去合并就可以了。如果其中有任意一个是空树就返回即可。

在实际应用的时候,我们往往是可以保证合并的前提的。因为要不然一开始是空树,要不然一开始会像建立splay一样建树,保证权值是像二叉搜索树一样的。

int merge(int x,int y)
{
if(!x || !y) return x | y;
pushdown(x),pushdown(y);
if(t[x].rk < t[y].rk) {t[x].rc = merge(t[x].rc,y),pushup(x);return x;}
else {t[y].lc = merge(x,t[y].lc),pushup(y);return y;}
}

2.\(split\)操作

与\(merge\)相对应,\(split\)就是把一棵平衡树分开,分裂成两棵。有两种分裂的方法,一种是按照权值分裂,一种按照子树大小分裂。

我们以按照权值分裂为例。对于$ split(u,k,x,y) $来说,我们要把一棵以u为根的平衡树分裂成以x,y为根的两棵平衡树,其中权值<=k的根为x,否则为y。首先我们判断当前的根的权值大小。如果<=k,那么就说明当前树的左子树应该全部以x为根,那么我们使x成为u,之后就剩下x的右子树需要进行分裂,那么我们使x成为x的右子树,y不变,继续向下分裂即可。注意这里要传址,因为x,y是一直在改变的。

否则的话,就说明当前的右子树应该全部以y为根。我们使y成为u,之后剩下y的左子树需要分裂,使y成为y的左子树,继续向下分裂即可。

按照子树大小也同理。

(权值)

void split(int u,int k,int &x,int &y)
{
if(!u) x = y = 0;
else
{
if(t[u].val <= k) x = u,split(t[u].ch[1],k,t[u].ch[1],y);
else y = u,split(t[u].ch[0],k,x,t[u].ch[0]);
pushup(u);
}
}

(子树大小)

void splits(int u,int k,int &x,int &y)
{
if(!u) {x = y = 0;return;}
pushdown(u);
if(t[t[u].lc].size >= k) y = u,splits(t[u].lc,k,x,t[u].lc);
else x = u,splits(t[u].rc,k-t[t[u].lc].size-1,t[u].rc,y);
pushup(u);
}

\(pushdown\)和\(pushup\)不是啥时候都要写的,看题的情况。

这就是最核心的操作。其他的什么查找第k大就二分一下就完事了。

那我们来练练手。普通平衡树和文艺平衡树。这个现在就很容易啦。每次操作先split提取出要操作的子树,处理完merge回去就行。注意区间翻转要时刻进行pushdown。

代码不贴了。

下一个话题就是进行可持久化。

传送门

感觉像是很简单的样子?不更改树的形态的话,那么我们只需要每次copy一下root,之后在当前的root上做各种操作就好了。

不过这是一个伪的可持久化,虽然在luogu上能得到96分……因为在\(merge\)和\(split\)的过程中,会使得你原来的版本发生变化。所以我们的解决办法是,每次新建立一个节点,新的版本在新建立的节点上进行操作。不过这样空间开销会变大一些。

其他的操作就没啥特殊的啦。

#include<bits/stdc++.h>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')
#define I inline using namespace std;
typedef long long ll;
const int M = 500005;
const int mod = 1e9+7;
const int INF = 2147483647; I int read()
{
int ans = 0,op = 1;char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') op = -1;ch = getchar();}
while(ch >= '0' && ch <= '9') ans = ans * 10 + ch - '0',ch = getchar();
return ans * op;
} struct tree
{
int ch[2],fa,size,val,rk;
}t[M<<6]; int root[M],x,y,z,n,tim,op,w,tot,v; I int newnode(int x)
{
t[++tot].size = 1,t[tot].val = x,t[tot].rk = rand();
return tot;
} I void pushup(int x){t[x].size = t[t[x].ch[0]].size + t[t[x].ch[1]].size + 1;} int merge(int x,int y)
{
if(!x || !y) return x | y;
if(t[x].rk < t[y].rk)
{
int p = ++tot;t[p] = t[x];
t[p].ch[1] = merge(t[p].ch[1],y),pushup(p);
return p;
}
else
{
int p = ++tot;t[p] = t[y];
t[p].ch[0] = merge(x,t[p].ch[0]),pushup(p);
return p;
}
} void split(int u,int v,int &x,int &y)
{
if(!u) x = y = 0;
else
{
if(t[u].val <= v) x = ++tot,t[x] = t[u],split(t[x].ch[1],v,t[x].ch[1],y),pushup(x);
else y = ++tot,t[y] = t[u],split(t[y].ch[0],v,x,t[y].ch[0]),pushup(y);
}
} I int kth(int x,int k)
{
while(1)
{
if(t[t[x].ch[0]].size >= k) x = t[x].ch[0];
else if(k == t[t[x].ch[0]].size + 1) return x;
else k -= (t[t[x].ch[0]].size + 1),x = t[x].ch[1];
}
} int main()
{
srand(time(NULL));
n = read();
rep(i,1,n)
{
tim = read(),op = read(),v = read();
root[i] = root[tim];
if(op == 1) split(root[i],v,x,y),root[i] = merge(merge(x,newnode(v)),y);
if(op == 2)
{
split(root[i],v,x,y),split(x,v-1,x,z);
z = merge(t[z].ch[0],t[z].ch[1]);
root[i] = merge(merge(x,z),y);
}
if(op == 3) split(root[i],v-1,x,y),printf("%d\n",t[x].size + 1),root[i] = merge(x,y);
if(op == 4) printf("%d\n",t[kth(root[i],v)].val);
if(op == 5)
{
split(root[i],v-1,x,y);
if(t[x].size == 0) printf("%d\n",-INF);
else printf("%d\n",t[kth(x,t[x].size)].val);
root[i] = merge(x,y);
}
if(op == 6)
{
split(root[i],v,x,y);
if(t[y].size == 0) printf("%d\n",INF);
else printf("%d\n",t[kth(y,1)].val);
root[i] = merge(x,y);
}
}
return 0;
}

这个都有了,那么再支持一个区间翻转也不是很难。

传送门

这题就比较复杂一点,区间旋转的时候需要像做文艺平衡树那题时候一样翻转,注意需要时刻pushdown。每次在pushdown的时候,都需要生成一个你左右儿子的新版本,否则的话会因为交换而修改原来版本的情况。

说实话,跑的有点慢,而且比较不稳定。

// luogu-judger-enable-o2
#include<bits/stdc++.h>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')
#define I inline using namespace std;
typedef long long ll;
const int M = 500005;
const int mod = 1e9+7;
const int INF = 2147483647; I ll read()
{
ll ans = 0,op = 1;char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') op = -1;ch = getchar();}
while(ch >= '0' && ch <= '9') ans = ans * 10 + ch - '0',ch = getchar();
return ans * op;
} struct tree
{
int ch[2],val,size,fa,rk,rev;
ll sum;
}t[M<<6]; int root[M],n,m,x,y,z,p,v,op,tim,l,r,tot;
ll last; I int newnode(int x)
{
t[++tot].size = 1,t[tot].val = t[tot].sum = x,t[tot].rk = rand();
return tot;
} I int copy(int x)
{
int p = newnode(0);
t[p] = t[x];
return p;
} I void pushup(int x)
{
t[x].size = t[t[x].ch[0]].size + t[t[x].ch[1]].size + 1;
t[x].sum = t[t[x].ch[0]].sum + t[t[x].ch[1]].sum + t[x].val;
} I void pushdown(int x)
{
if(!t[x].rev) return;
if(t[x].ch[0]) t[x].ch[0] = copy(t[x].ch[0]);
if(t[x].ch[1]) t[x].ch[1] = copy(t[x].ch[1]);
swap(t[x].ch[0],t[x].ch[1]),t[x].rev = 0;
if(t[x].ch[0]) t[t[x].ch[0]].rev ^= 1;
if(t[x].ch[1]) t[t[x].ch[1]].rev ^= 1;
} int merge(int x,int y)
{
if(!x || !y) return x | y;
pushdown(x),pushdown(y);
if(t[x].rk < t[y].rk) {t[x].ch[1] = merge(t[x].ch[1],y),pushup(x);return x;}
else {t[y].ch[0] = merge(x,t[y].ch[0]),pushup(y);return y;}
} void splits(int u,int v,int &x,int &y)
{
if(!u) x = y = 0;
else
{
pushdown(u);
if(t[t[u].ch[0]].size >= v) y = copy(u),splits(t[y].ch[0],v,x,t[y].ch[0]),pushup(y);
else x = copy(u),splits(t[x].ch[1],v - t[t[u].ch[0]].size - 1,t[x].ch[1],y),pushup(x);
}
} I void rever(int k,int l,int r)
{
int a,b,c,d;
splits(root[k],r,a,b),splits(a,l-1,c,d);
t[d].rev ^= 1;
root[k] = merge(merge(c,d),b);
} I void query(int k,int l,int r)
{
int a,b,c,d;
splits(root[k],r,a,b),splits(a,l-1,c,d);
printf("%lld\n",t[d].sum),last = t[d].sum;
root[k] = merge(merge(c,d),b);
} int main()
{
srand(time(NULL));
n = read();
rep(i,1,n)
{
tim = read(),op = read(),root[i] = root[tim];
if(op == 1)
{
p = read() ^ last,v = read() ^ last;
splits(root[i],p,x,y);
root[i] = merge(merge(x,newnode(v)),y);
}
if(op == 2)
{
p = read() ^ last,splits(root[i],p,x,y),splits(x,p-1,x,z);
root[i] = merge(x,y);
}
if(op == 3) l = read() ^ last,r = read() ^ last,rever(i,l,r);
if(op == 4) l = read() ^ last,r = read() ^ last,query(i,l,r);
}
return 0;
}

\(fhq-treap\)的简介就先到这里吧。orz,这果然是神仙数据结构啊。

fhq-treap简介的更多相关文章

  1. 在平衡树的海洋中畅游(四)——FHQ Treap

    Preface 关于那些比较基础的平衡树我想我之前已经介绍的已经挺多了. 但是像Treap,Splay这样的旋转平衡树码亮太大,而像替罪羊树这样的重量平衡树却没有什么实际意义. 然而类似于SBT,AV ...

  2. 浅谈fhq treap

    一.简介 fhq treap 与一般的treap主要有3点不同 1.不用旋转 2.以merge和split为核心操作,通过它们的组合实现平衡树的所有操作 3.可以可持久化 二.核心操作 代码中val表 ...

  3. fhq treap 学习笔记

    序 今天心血来潮,来学习一下fhq treap(其实原因是本校有个OIer名叫fh,当然不是我) 简介 fhq treap 学名好像是"非旋转式treap及可持久化"...听上去怪 ...

  4. 简析平衡树(四)——FHQ Treap

    前言 好久没码过平衡树了! 这次在闪指导的指导下学会了\(FHQ\ Treap\),一方面是因为听说它可以可持久化,另一方面则是因为听说它是真的好写. 简介 \(FHQ\ Treap\),又称作非旋\ ...

  5. fhq treap最终模板

    新学习了fhq treap,厉害了 先贴个神犇的版, from memphis /* Treap[Merge,Split] by Memphis */ #include<cstdio> # ...

  6. NOI 2002 营业额统计 (splay or fhq treap)

    Description 营业额统计 Tiger最近被公司升任为营业部经理,他上任后接受公司交给的第一项任务便是统计并分析公司成立以来的营业情况. Tiger拿出了公司的账本,账本上记录了公司成立以来每 ...

  7. 【POJ2761】【fhq treap】A Simple Problem with Integers

    Description You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. On ...

  8. 【fhq Treap】bzoj1500(听说此题多码上几遍就能不惧任何平衡树题)

    1500: [NOI2005]维修数列 Time Limit: 10 Sec  Memory Limit: 64 MBSubmit: 15112  Solved: 4996[Submit][Statu ...

  9. 「FHQ Treap」学习笔记

    话说天下大事,就像fhq treap —— 分久必合,合久必分 简单讲一讲.非旋treap主要依靠分裂和合并来实现操作.(递归,不维护fa不维护cnt) 合并的前提是两棵树的权值满足一边的最大的比另一 ...

  10. FHQ Treap摘要

    原理 以随机数维护平衡,使树高期望为logn级别 不依靠旋转,只有两个核心操作merge(合并)和split(拆分) 因此可持久化 先介绍变量 ; int n; struct Node { int v ...

随机推荐

  1. SilverLight:布局(2)GridSplitter(网格分割)垂直分割、水平分割

    ylbtech-SilverLight-Layout: 布局(2)GridSplitter(网格分割)垂直分割.水平分割 A, Splitter(分割)对象之 GridSplitter(网格分割)1: ...

  2. Dance In Heap(二):一些堆利用的方法(上)

    0×00 前面的话 在前面的文章里我们稍微有点啰嗦的讲解了堆中的一些细节,包括malloc.free的详细过程,以及一些检查保护机制,那在这篇文章里,我们就开始结合这些机制,以64位为例来看一看如何对 ...

  3. [影像技术与PACS] 从技术角度看国内部份PACS厂商

    天健PACS较早从事影像医院处理系统,为国外系统或设备以OEM方式提供软件模块.天健的PACS里面三维重建.容积重建.血管分析.虚拟腔镜.头部灌注等部分是用西安盈谷科技的,手术麻醉和重症监护系统是奥迪 ...

  4. angular 资源路径问题

    1.templateUrl .component("noData",{ templateUrl:"components/noData.html" // 注意相对 ...

  5. 深度探究apk安装过程

    一.先验知识 0.PcakageaManagerService版本号变化 1.概述 2.PackageManagerService服务启动流程 3. PackageManagerService入口 二 ...

  6. 实现TextView中link的点击效果

    朋友们,你们在TextView处理link的时候是不是一直被苦逼的android默认的方式困扰?每次点击link的时候,点击效果是整个textview来响应.非常烂吧?原因就不多赘述了. 那么以下这个 ...

  7. 邻接表的使用及和vector的比較

    这几天碰到一些对建边要求挺高的题目.而vector不好建边,所以学习了邻接表.. 以下是我对邻接表的一些看法. 邻接表的储存方式 邻接表就是就是每一个节点的一个链表,而且是头插法建的链表,这里我们首先 ...

  8. HAProxy简单使用

    一.HAProxy简介及定位         HAProxy 是一款基于TCP和HTTP应用的具备高可用行且负载均衡的代理软件.HAProxy是完全免费的,借助HAProxy可以快速.可靠地提供基于T ...

  9. H5实现多图片预览上传,可点击可拖拽控件介绍

    版权声明:欢迎转载,请注明出处:http://blog.csdn.net/weixin_36380516 在做图片上传时发现一个蛮好用的控件,支持多张图片同时上传,可以点击选择图片,也可以将图片拖拽到 ...

  10. Java依照List内存储的对象的某个字段进行排序

    关键点:将List内存储的对象实现Comparable类.重写它的compareTo()方法就可以 Bean: package chc; public class StuVo implements C ...