一种二叉树,这棵树满足任意一个节点,它的左儿子的权值<自己的权值<右儿子的权值
这种树叫做二叉查找树,这个概念应该在初赛中见过了吧
Splay就是利用这个结构来实现的
模板题的7大变量
inline void clear(int x){
f[x] = key[x] = size[x] = recy[x] = son[x][0] = son[x][1] = 0;
//5个数组全部清零
}
int get(int x){
return son[f[x]][1] == x;
//如果自己是父亲的右儿子,返回1;否则返回0(0为左儿子,1为右儿子)
}
inline void update(int x){
if (x){//如果这个点存在
size[x] = recy[x];//自己重复的次数先累计
if (son[x][0]) size[x] += size[son[x][0]];
if (son[x][1]) size[x] += size[son[x][1]];
//如果存在儿子,把儿子的size累积到自己
//然后发现一个问题,更新自己的size时,必须保证儿子的size是正确的
//所以后面的操作,当牵扯到儿子和父亲时,应该先更新新儿子,后更新新父亲
}
}
void connect(int x, int y, int z){//x连到y下面,关系为z
if (x) f[x] = y;//存在x,则x的父亲为y
if (y) son[y][z] = x;//存在y,y的z关系儿子为x
}
看图
情况1
发现:绿点上旋,绿点是父亲橙点的左儿子,所以绿点的右儿子紫点变成橙点的左儿子,橙点变成绿点的右儿子,绿点变成红点的左儿子
情况2
发现:绿点上旋,绿点是父亲黄点的右儿子,所以绿点的左儿子蓝点变成黄点的右儿子,黄点变成绿点的左儿子,绿点变成红点的右儿子
void rotate(int x){//上旋x
int fa = f[x], ffa = f[fa], m = get(x), n = get(fa);//确定x,fa的关系
connect(son[x][m ^ 1], fa, m);//把要转的儿子(关系为m^1的儿子)转到父亲下,关系为m
connect(fa, x, m ^ 1);//把父亲转到自己下面,关系为m^1
connect(x, ffa, n);//把自己转到父亲的父亲下,关系为n
update(fa), update(x);//先更新fa,再更新自己,可以自己想想为什么是这个顺序
}
把一个点一直上旋,旋到规定点,此题全部是旋到根节点,所以默认旋到根节点
设当前splay的点为x
如果x的父亲是根,直接旋;
否则分两种情况:
为什么分两种情况?可以自行画图看一看
发现按照上述旋转,每次splay以后,整棵树十分的平衡!
(接近于完全二叉树)
如果不分情况,直接无脑上旋,则会结构变得比较乱
splay操作是算法的核心,它保证的二叉树的随机性,平衡性
所以,当你在打splay的时候,记住一条准则:有事没事splay一下
void splay(int x){
for (int fa; fa = f[x]; rotate(x))//每次总是旋转自己
if (f[fa]) rotate(get(x) == get(fa) ? fa : x);//如果有爷爷(父亲的父亲),看父亲与父亲的父亲的关系决定转哪个
rt = x;//别忘了,把根赋为当前点
}
void insert(int x){
if (!rt){//树中没有一个节点
rt = ++sz;
key[rt] = x;
size[rt] = recy[rt] = 1;
son[rt][0] = son[rt][1] = 0;//赋初值
return;
}
int now = rt, fa = 0;
while (1){
if (key[now] == x){//树中已有此点,重复+1
++recy[now];
update(now); update(fa);
splay(now);//splay一下,保证平衡
return;
}
fa = now, now = son[now][x > key[now]];//满足二叉查找树的性质,往下跑
if (!now){
++sz;
key[sz] = x;
size[sz] = recy[sz] = 1;//赋初值
f[sz] = fa;//父亲是fa
son[fa][x > key[fa]] = sz;//更新父亲的新儿子
update(fa);//更新父亲的size
splay(sz);//splay一下,保证平衡
return;
}
}
}
int find(int x){//查排名
int now = rt, ans = 0;
while (1){
if (x < key[now]){
now = son[now][0]; continue;//在左子树中
}
ans += size[son[now][0]];//排名加上左子树节点个数
if (x == key[now]){ splay(now); return ans + 1; }//值等于当前点,splay一下,保证平衡,排名+1为当前排名
ans += recy[now];//排名加上当前节点的数的个数
now = son[now][1];//在右子树中
}
}
int kth(int x){//查找排名为x的数
int now = rt;
while (1){
if (son[now][0] && x <= size[son[now][0]]){//在左子树中
now = son[now][0]; continue;
}
if (son[now][0]) x -= size[son[now][0]];//存在左儿子,排名减去左子树节点数
if (x <= recy[now]){ splay(now); return key[now]; }//说明就是当前点,splay一下,保证平衡,退出
x -= recy[now];//排名减去当前节点数的个数
now = son[now][1];//在右子树中
}
}
采用全新的方法
首先,插入目标新节点,使该节点在根上
那么它的前驱为左子树中最大的那个
后继为右子树中最小的那个
最后,当然要删掉刚才插入的节点
至于为什么,想想就知道了
if (opt == 5){
insert(x); printf("%d\n", key[pre()]); del(x);//insert->del
}
if (opt == 6){
insert(x); printf("%d\n", key[nxt()]); del(x);//insert->del
}
int pre(){//前驱为左子树中最大的那个
int now = son[rt][0];
while (son[now][1]) now = son[now][1];
return now;
}
int nxt(){//后继为右子树中最小的那个
int now = son[rt][1];
while (son[now][0]) now = son[now][0];
return now;
}
void del(int x){
int no_use = find(x);//find主要是把当前数的对应点找到,然后旋到根,返回值的排名在这里没用
if (recy[rt] > 1){//情况1:有重复,重复-1,更新,退出
--recy[rt];
update(rt);
return;
}
//接下来都是没有重复的情况
if (!son[rt][0] && !son[rt][1]){//情况2:没有儿子,直接清空
clear(rt);
rt = 0;
return;
}
if (!son[rt][0]){//情况3:没有左儿子,只有右儿子,右儿子变成根,清除自己
int tmp = rt;
f[rt = son[rt][1]] = 0;
clear(tmp);
return;
}
if (!son[rt][1]){//情况4:没有右儿子,只有左儿子,左儿子变成根,清除自己
int tmp = rt;
f[rt = son[rt][0]] = 0;
clear(tmp);
return;
}
//情况5:两个儿子都有,这是需要一个很简便的做法
//把前驱splay到根,保持左子树其他节点不用动
//原根右儿子变成前驱的右儿子
//原根功成身退,清除掉
//最后对前驱的size进行更新
int tmp = rt, left = pre();
splay(left);
connect(son[tmp][1], rt, 1);
clear(tmp);
update(rt);
}
全在上面了,直接Code:
#include
#define maxn 100010
using namespace std;
int sz, rt, f[maxn], key[maxn], size[maxn], recy[maxn], son[maxn][2];
inline int read(){
int s = 0, w = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
return s * w;
}
void clear(int x){
f[x] = key[x] = size[x] = recy[x] = son[x][0] = son[x][1] = 0;
//5个数组全部清零
}
int get(int x){
return son[f[x]][1] == x;
//如果自己是父亲的右儿子,返回1;否则返回0(0为左儿子,1为右儿子)
}
void update(int x){
if (x){//如果这个点存在
size[x] = recy[x];//自己重复的次数先累计
if (son[x][0]) size[x] += size[son[x][0]];
if (son[x][1]) size[x] += size[son[x][1]];
//如果存在儿子,把儿子的size累积到自己
//然后发现一个问题,更新自己的size时,必须保证儿子的size是正确的
//所以后面的操作,当牵扯到儿子和父亲时,应该先更新新儿子,后更新新父亲
}
}
void connect(int x, int y, int z){//x连到y下面,关系为z
if (x) f[x] = y;//存在x,则x的父亲为y
if (y) son[y][z] = x;//存在y,y的z关系儿子为x
}
void rotate(int x){//上旋x
int fa = f[x], ffa = f[fa], m = get(x), n = get(fa);//确定x,fa的关系
connect(son[x][m ^ 1], fa, m);//把要转的儿子转到父亲下,关系为m
connect(fa, x, m ^ 1);//把父亲转到自己下面,关系为m^1
connect(x, ffa, n);//把自己转到父亲的父亲下,关系为n
update(fa), update(x);//先更新fa,再更新自己,可以自己想想为什么是这个顺序
}
void splay(int x){
for (int fa; fa = f[x]; rotate(x))//每次总是旋转自己
if (f[fa]) rotate(get(x) == get(fa) ? fa : x);//如果有爷爷(父亲的父亲),看父亲与父亲的父亲的关系决定转哪个
rt = x;//别忘了,把根赋为当前点
}
void insert(int x){
if (!rt){//树中没有一个节点
rt = ++sz;
key[rt] = x;
size[rt] = recy[rt] = 1;
son[rt][0] = son[rt][1] = 0;//赋初值
return;
}
int now = rt, fa = 0;
while (1){
if (key[now] == x){//树中已有此点,重复+1
++recy[now];
update(now); update(fa);
splay(now);//splay一下,保证平衡
return;
}
fa = now, now = son[now][x > key[now]];//满足二叉查找树的性质,往下跑
if (!now){
++sz;
key[sz] = x;
size[sz] = recy[sz] = 1;//赋初值
f[sz] = fa;//父亲是fa
son[fa][x > key[fa]] = sz;//更新父亲的新儿子
update(fa);//更新父亲的size
splay(sz);//splay一下,保证平衡
return;
}
}
}
int find(int x){//查排名
int now = rt, ans = 0;
while (1){
if (x < key[now]){
now = son[now][0]; continue;//在左子树中
}
ans += size[son[now][0]];//排名加上左子树节点个数
if (x == key[now]){ splay(now); return ans + 1; }//值等于当前点,splay一下,保证平衡,排名+1为当前排名
ans += recy[now];//排名加上当前节点的数的个数
now = son[now][1];//在右子树中
}
}
int kth(int x){//查找排名为x的数
int now = rt;
while (1){
if (son[now][0] && x <= size[son[now][0]]){//在左子树中
now = son[now][0]; continue;
}
if (son[now][0]) x -= size[son[now][0]];//存在左儿子,排名减去左子树节点数
if (x <= recy[now]){ splay(now); return key[now]; }//说明就是当前点,splay一下,保证平衡,退出
x -= recy[now];//排名减去当前节点数的个数
now = son[now][1];//在右子树中
}
}
int pre(){//前驱为左子树中最大的那个
int now = son[rt][0];
while (son[now][1]) now = son[now][1];
return now;
}
int nxt(){//后继为右子树中最小的那个
int now = son[rt][1];
while (son[now][0]) now = son[now][0];
return now;
}
void del(int x){
int no_use = find(x);//find主要是把当前数的对应点找到,然后旋到根,返回值的排名在这里没用
if (recy[rt] > 1){//情况1:有重复,重复-1,更新,退出
--recy[rt];
update(rt);
return;
}
//接下来都是没有重复的情况
if (!son[rt][0] && !son[rt][1]){//情况2:没有儿子,直接清空
clear(rt);
rt = 0;
return;
}
if (!son[rt][0]){//情况3:没有左儿子,只有右儿子,右儿子变成根,清除自己
int tmp = rt;
f[rt = son[rt][1]] = 0;
clear(tmp);
return;
}
if (!son[rt][1]){//情况4:没有右儿子,只有左儿子,左儿子变成根,清除自己
int tmp = rt;
f[rt = son[rt][0]] = 0;
clear(tmp);
return;
}
//情况5:两个儿子都有,这是需要一个很简便的做法
//把前驱splay到根,保持左子树其他节点不用动
//原根右儿子变成前驱的右儿子
//原根功成身退,清除掉
//最后对前驱的size进行更新
int tmp = rt, left = pre();
splay(left);
connect(son[tmp][1], rt, 1);
clear(tmp);
update(rt);
}
int main(){
int M = read();
while (M--){
int opt = read(), x = read();
if (opt == 1) insert(x);
if (opt == 2) del(x);
if (opt == 3) printf("%d\n", find(x));
if (opt == 4) printf("%d\n", kth(x));
if (opt == 5){
insert(x); printf("%d\n", key[pre()]); del(x);
}
if (opt == 6){
insert(x); printf("%d\n", key[nxt()]); del(x);
}
}
return 0;
}
建议完全理解splay的代码
多看看
不知道代码哪错了建议重构代码
建议过些天复习复习
说白了,区间翻转
发现一棵二叉查找树,中序遍历是有序的
既然这样,何不先按顺序加入二叉树?
对于每个操作 [ l , r ] [l,r] [l,r],找到 k t h ( l − 1 ) , k t h ( r + 1 ) kth(l-1),kth(r+1) kth(l−1),kth(r+1)
分别记为 p r e , n x t pre, nxt pre,nxt
把 p r e pre pre旋到根,把 n x t nxt nxt旋到根的(右)儿子
发现: n x t nxt nxt的左子树节点集合= [ l , r ] [l,r] [l,r]节点集合
因为对于任何一个 x ∈ [ l , r ] x∈[l,r] x∈[l,r],都有 v p r e < v x < v n x t v_{pre}
必定都在nxt左子树中,且nxt左子树没有别的节点
说明整个子树翻转一下就行啦
怎么做呢?采用打标记的方法
在nxt左儿子上打一个tag, t a g = 1 tag=1 tag=1说明这个区间需要翻转
下传标记,在每次求kth,splay的时候pushdown一下,反正能pushdown就pushdown,多了不会慢太多,少了万一wa了呢~~,其实自己分析一下也可以知道哪些需要pushdown,不过都下传保险嘛,万一分析错了呢
如何pushdown?首先儿子的tag^=1,至于为什么是亦或自己想想就行
然后交换两个儿子的编号(因为翻转),最后自己的tag=0
首先,需要注意,插入-inf与inf,防止pre和nxt找不到
for (int i = 1; i <= n + 2; ++i) insert(i);
//1~n全体+1,1为-inf,n+2为inf
void insert(int x){
int now = rt, fa = 0;
while(now) fa = now, now = son[now][x > key[now]];
//插入新节点,知道可以插入为止
now = ++sz;
key[now] = x;
f[now] = fa;
if (fa) son[fa][x > key[fa]] = now;
size[now] = 1;
son[now][0] = son[now][1] = 0;
splay(now, 0);//旋到根,保持平衡
}
void connect(int x, int y, int z){ f[x] = y, son[y][z] = x; }
void rotate(int x){
int fa = f[x], ffa = f[fa], m = get(x), n = get(fa);
connect(x, ffa, n);
connect(son[x][m ^ 1], fa, m);
connect(fa, x, m ^ 1);
update(fa); update(x);
}
//注意这里的splay跟上面普通splay不一样
void splay(int x, int goal){
int len = 0;
for (int i = x; i; i = f[i]) q[++len] = i;
for (int i = len; i; --i) pushdown(q[i]);
//先把可能经过的节点全部下传一遍,注意下传顺序
while (f[x] != goal){//没旋到goal
int fa = f[x];
if (f[fa] != goal) rotate(get(x) == get(fa) ? fa : x);
rotate(x);
}
if (!goal) rt = x;//是不是旋到根节点
}
int kth(int x){
int now = rt;
while (1){
pushdown(now);//下传标记
if (size[son[now][0]] >= x) now = son[now][0]; else{
x -= size[son[now][0]];
if (x == 1) return now;
--x, now = son[now][1];
}
}
}
翻转区间 [ l , r ] [l,r] [l,r]
void work(int l, int r){
l = kth(l); r = kth(r + 2);//找到pre,nxt
splay(l, 0); splay(r, l);//上旋
tag[son[r][0]] ^= 1;//打标记,注意这里也是亦或
}
void pushdown(int x){
if (tag[x]){//如果有标记
tag[x] = 0;//自己变为0
tag[son[x][0]] ^= 1;
tag[son[x][1]] ^= 1;//下传
swap(son[x][0], son[x][1]);//翻转儿子
}
}
void write(int x){
pushdown(x);//下传
if (son[x][0]) write(son[x][0]);//中序遍历 左中右
if (key[x] > 1 && key[x] < n + 2) printf("%d ", key[x] - 1);
//注意一开始都+1,最后-1
if (son[x][1]) write(son[x][1]);
}
Code:
#include
#define maxn 100010
using namespace std;
int f[maxn], key[maxn], son[maxn][2], size[maxn], q[maxn], tag[maxn], n, m, rt, sz;
inline int read(){
int s = 0, w = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
return s * w;
}
void update(int x){
size[x] = 1;
if (son[x][0]) size[x] += size[son[x][0]];
if (son[x][1]) size[x] += size[son[x][1]];
}
int get(int x){ return son[f[x]][1] == x; }
void pushdown(int x){
if (tag[x]){
tag[x] = 0;
tag[son[x][0]] ^= 1;
tag[son[x][1]] ^= 1;
swap(son[x][0], son[x][1]);
}
}
void connect(int x, int y, int z){ f[x] = y, son[y][z] = x; }
void rotate(int x){
int fa = f[x], ffa = f[fa], m = get(x), n = get(fa);
connect(x, ffa, n);
connect(son[x][m ^ 1], fa, m);
connect(fa, x, m ^ 1);
update(fa); update(x);
}
void splay(int x, int goal){
int len = 0;
for (int i = x; i; i = f[i]) q[++len] = i;
for (int i = len; i; --i) pushdown(q[i]);
while (f[x] != goal){
int fa = f[x];
if (f[fa] != goal) rotate(get(x) == get(fa) ? fa : x);
rotate(x);
}
if (!goal) rt = x;
}
void insert(int x){
int now = rt, fa = 0;
while(now) fa = now, now = son[now][x > key[now]];
now = ++sz;
key[now] = x;
f[now] = fa;
if (fa) son[fa][x > key[fa]] = now;
size[now] = 1;
son[now][0] = son[now][1] = 0;
splay(now, 0);
}
int kth(int x){
int now = rt;
while (1){
pushdown(now);
if (size[son[now][0]] >= x) now = son[now][0]; else{
x -= size[son[now][0]];
if (x == 1) return now;
--x, now = son[now][1];
}
}
}
void work(int l, int r){
l = kth(l); r = kth(r + 2);
splay(l, 0); splay(r, l);
tag[son[r][0]] ^= 1;
}
void write(int x){
pushdown(x);
if (son[x][0]) write(son[x][0]);
if (key[x] > 1 && key[x] < n + 2) printf("%d ", key[x] - 1);
if (son[x][1]) write(son[x][1]);
}
int main(){
n = read(), m = read();
for (int i = 1; i <= n + 2; ++i) insert(i);
while (m--){
int l = read(), r = read();
work(l, r);
}
write(rt);
return 0;
}
排版有些乱(⊙﹏⊙汗
我还在寻找自己的风格,每天文化课有些紧,上机时间也不多,题目也做的不多
不是特别熟练也请谅解
[CQOI2014]排序机械臂:练手好题,加深理解
[NOI2004]郁闷的出纳员:有点思维难度
送花:裸题,你需要很快的秒了
宝石管理系统:如果短时间内完成,说明你掌握的还不错
[HNOI2004]宠物收养场:比较简单的Splay
[ZJOI2006]书架:有点烦,有点难调
[NOI2003]文本编辑器:非常基本的操作训练
[HNOI2012]永无乡:splay dsu
[NOIp2017]列队:较毒瘤,好题
序列终结者:裸题,秒杀
[HNOI2002]营业额统计:裸题++