题目链接如下所示:
Problem - 1890
这题可把我给整吐了,我看了大概两个下午吧,看大家说这是splay水题,是不是我不知道,但是我是真的很水。
首先,我们得了解一下splay树,这个大家自行百度吧...
这里有个关于区间操作的讲解或许有帮助:
Splay处理区间操作——翻转操作(Reverse) - 笨蛋花的小窝qwq - 洛谷博客
然后这里先介绍一下整体思路:
1.对于一个输入的序列,按照输入的标号建立平衡二叉树
2.把<输入值,输入顺序标号>按照输入值优先级大于顺序标号的情况,排序
3.按照排序后的顺序,将这个点的输入顺序标号用splay操作旋转到根节点
4.更新机械臂的旋转操作
5.输出排序,删除根节点
我们一点一点来说:
rev这个后面提,具体就是代表是否需要把旋转传递到下面的子树中去。
pre就是父节点的id,size[i]代表i这个节点为根的子树的大小。
tree用来存一个节点的左右子树的id。
const int N=100010;
int rev[N],pre[N],size[N];
int tree[N][2];
int root;
struct Node{int val;int id;bool operator < (const Node &A) const{if (val==A.val) return id
首先,怎么建立一个平衡二叉树呢,用数组的形式。其实就是用二分的方法,这里用传引用的方式直接修改子节点。代码如下所示:
void newnode(int &x,int fa,int val){x=val;pre[x]=fa;size[x]=1;rev[x]=0;tree[x][0]=tree[x][1]=0;
}void buildtree(int &x,int l,int r,int fa){if (l>r) return;int mid=(l+r)>>1;newnode(x,fa,mid);buildtree(tree[x][0],l,mid-1,x);buildtree(tree[x][1],mid+1,r,x);pushup(x);
}void init(int n){root=0;tree[root][0]=tree[root][1]=pre[root]=size[root]=0;buildtree(root, 1, n, 0);
}
这东西本身其实没啥困难的,无非就是把子节点旋转一下然后更新一些指向以及子树的大小,但是他引入了机械臂旋转这样一个操作,就很恶心。先看一下代码吧:
void pushup(int x){size[x]=1+size[tree[x][0]]+size[tree[x][1]];
}void update_rev(int x){if (!x) return;swap(tree[x][0],tree[x][1]);rev[x]^=1;
}void pushdown(int x){if (!rev[x]) return;update_rev(tree[x][0]);update_rev(tree[x][1]);rev[x]=0;
}// c=0为左旋 1为右旋
void rotate(int x,int c){int y=pre[x];pushdown(y);pushdown(x);// 原来x的子树放在y下面tree[y][!c]=tree[x][c];pre[tree[x][c]]=y;// 原来y和pre[y]的指向改变if (pre[y]){tree[pre[y]][tree[pre[y]][1]==y]=x;}pre[x]=pre[y];// 修改x和y之间的指向tree[x][c]=y;pre[y]=x;pushup(y);
}void splay(int x, int goal){pushdown(x);while(pre[x]!=goal){if (pre[pre[x]]==goal){pushdown(pre[x]);pushdown(x);rotate(x,tree[pre[x]][0]==x);}else{pushdown(pre[pre[x]]);pushdown(pre[x]);pushdown(x);int y=pre[x];int c=(tree[pre[y]][0]==y);if (tree[y][c]==x){//不共线rotate(x,!c);rotate(x,c);}else{rotate(y,c);rotate(x,c);}}}pushup(x);if (goal==0){root=x;}
}
这里边让我想了最久的就是这个pushdown,就是说这个“懒旋转”,他是这样的,每次我们不是需要把一个节点的标号对应的节点旋转到根吗,然后我们要明白,平衡二叉树的旋转并不改变中序遍历的结果,意思是他左边的子树按照中序遍历一遍那就是在他左边的序列的顺序啊!然后我们想把左边的序列全部翻转对吧,那么我们先在左子树的根上标记一下要翻转的信号,也就是先update_rev一下左子树的根,然后用pushdown传递下去,那么不就成功翻转了序列吗,因为第一次我们并没有翻转全部的序列,而是在后续操作中一步一步翻转,所以我们把这个叫做“懒旋转”捏。
这里旋转的顺序,我们最好就是把前面的旋转完了,这样就可以有抵消的效果,来达到减少操作数的效果。
如果根节点无左子树,那么直接修改根节点是右子树就行。
如果有左子树,那么找到左孩子的最右节点作为根节点,然后把这个节点旋转到根节点下面孩子的位置,修改这个节点的指向,最后把根的右子树和这个左孩子连接起来就好辣!
代码如下所示:
int get_max(int x){pushdown(x);while (tree[x][1]){x=tree[x][1];pushdown(x);}return x;
}void del_root(){if (!tree[root][0]){root=tree[root][1];pre[root]=0;}else{int m=get_max(tree[root][0]);splay(m,root);tree[m][1]=tree[root][1];pre[tree[root][1]]=m;root=m;pre[root]=0;pushup(root);}
}
最后代码如下所示:
#include
#include
#include
#include
#include
#include
#include
#include
#include