文艺平衡树


文艺平衡树

P3391 【模板】文艺平衡树

数组变量名意义

const int o_o=1e5+10;//数组下标大小 
int n,m;
struct po{
	int f;//父节点 
	int l_n;//懒标记 
	int l,r;//左右儿子 
	int s_z;//子树大小 
}t_r[o_o];
int g_g;//根节点 

建树

和不同的平衡术不同点在于,它的表示变成区间。

      (4)
      / \
     /   \
    /     \
  (2)     (6)
  / \     / \
(1) (3) (5) (7)

这是一棵初始化的“平衡树”,它的初始化很线段树的每个区间处理很相似。首先找出根节点(一班选区间中间的数),然后它的左右子树的意义在于:代表一个区间。而将数中序遍历则是原序列

$4$ 的左子树就代表 $[1,3]$,右子树代表 $[5,7]$。

void b_t(int l,int r,int f){
    if(l>r)return;//不在区间内,退出 
    int m_i=(l+r)>>1;
    if(m_i<f)t_r[f].l=m_i;//符合平衡树特性,存储左右节点 
	else t_r[f].r=m_i;
    t_r[m_i].f=f;//存父节点 
	t_r[m_i].s_z=1;//初始化节点大小 
    if(l==r)return;//子节点,不会再存在左右子树,退出 
    b_t(l,m_i-1,m_i);//左子树 
	b_t(m_i+1,r,m_i);//右子树 
    u_p(m_i);//更新当前节点 
}

节点更新信息维护及节点子树大小即可。

void u_p(int x){
    t_r[x].s_z=t_r[t_r[x].l].s_z+t_r[t_r[x].r].s_z+1;//左右子树大小的和 +1 
}

旋转

和平衡树的旋转方式相同,但由于我们要让一些节点有目的的向上转,所以要存父节点,和 这里 的思路相同,只是视角从父节点比变成了子节点(或孙节点)。

void b_l(int x,int y,int z){//左旋 
	//旋转(从根节点向上转) 
	t_r[y].r=t_r[x].l;
	t_r[t_r[y].r].f=y;
    t_r[x].l=y;
	t_r[y].f=x;//更新父节点 
	t_r[x].f=z;//更新父节点 
    u_p(x);//更新节点信息 
	u_p(y);
}
void b_r(int x,int y,int z){//右旋 
	//旋转(从根节点向上转) 
	t_r[y].l=t_r[x].r;
	t_r[t_r[y].l].f=y;
    t_r[x].r=y;
	t_r[y].f=x;//更新父节点 
	t_r[x].f=z;//更新父节点 
    u_p(x);//更新节点信息 
	u_p(y);
}

区间翻转

首先,整棵树一定是按照中序遍历输出的(存的时候就是这个目的存的)。接着我们目前会的只有旋转。但是,单纯的旋转并不能改变中序遍历的输出顺序。和 这里 讲的“相对位置”(降维打击变成一维数组的节点左右的关系)的理解相同。

既然是区间翻转那么一定是要有“相对位置”改变的。是时候破坏“先对位置”!,我们将一个节点的左右子树交换,就可以破坏“相对位置”。简单粗暴,行之有效

现在有了破坏的思路,那么要怎么做呢?

既然是区间翻转,树维护的就是区间,树的特性又是单调的(我们破坏的是值的单调性,但我们这棵树维护的是区间的单调性),那么我们又知道旋转不会破坏单调性,那么我们不难讲我们要旋转的区间放到一个子树中。如下:

      (4)
      / \
     /   \
    /     \
  (2)     (6)
  / \     / \
(1) (3) (5) (7)

现在我们要翻转 $[3,6]$。进行如下处理,将 $[3,6]$ 放到一个子树中。为了方便描述子树坐标,我们将 $2$ 转到根节点,$7$ 为 $2$ 的右节点,更具树的性质(单调性),$[3,6]$ 会变成 $7$ 的左子树。

先处理 $2$:

右旋:

    (2)
    / \
   /   \
  /     \
(1)     (4)
        / \
      (3) (6)
          / \
        (5) (7)

$2$ 到达目标地点。

再处理 $7$:

左旋:

    (2)
    / \
   /   \
  /     \
(1)     (4)
        / \
      (3) (7)
          / 
        (6) 
        /
      (5)

左旋:

    (2)
    / \
   /   \
  /     \
(1)     (7)
        / 
      (4) 
      / \   
    (3) (6) 
        /
      (5)

$7$ 到达目标地点。

现在我们可以将 $4$ 打上懒标记,这个懒标记是需要交换左右子树的懒标记。每次访问到这个点,需要释放并下传。为方便理解,我们释放所有懒标记。

    (2)
    / \
   /   \
  /     \
(1)     (7)
        / 
      (4) 
      / \   
    (6) (3) 
      \ 
      (5)

中序遍历:$1,2,6,5,4,3,7$。问题解决。

void x_t(int x,int &k){
    while(x!=k){//没有到达预期位置 
        int y=t_r[x].f;//原点的父节点 
		int z=t_r[y].f;//原节点的父节点的父节点
        if(y!=k){//父节点也没有到达预期 
            if((t_r[y].l==x)^(t_r[z].l==y))l_r(x,k);//一条链(都是左儿子或右儿子) 
            else l_r(y,k);//不是一条链 
        }
        l_r(x,k);//父节点到达预期,只用再旋转一次即可到达目的 
    }
}

将目标区间转移到一个子树,遍历整棵子树可以直接放心的打标记,不会影响到其他数据。

注意:我们每回要找 $l-1$ 和 $r+1$,但加入有一棵 $7$ 节点的树。翻转正好是 $[1,7]$,就会出问题。所以我们在一开始时手动加入两个点,$1$,$r+2$,而原区间变为 $[l+1,r+1]$,就可以解决这个问题。

代码

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<queue>
#include<vector>
#include<random>
#include<ctime>
using namespace std;
long long r_r(){//快读 
	long long k=0,f=1;
	char c=getchar();
	while(!isdigit(c)){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(isdigit(c)){
		k=(k<<1)+(k<<3)+(c^48);
		c=getchar();
	}
	return k*f;
}
const int o_o=1e5+10;//数组下标大小 
int n,m;
struct po{
	int f;//父节点 
	int l_n;//懒标记 
	int l,r;//左右儿子 
	int s_z;//子树大小 
}t_r[o_o];
int g_g;//根节点 
void u_p(int x){
    t_r[x].s_z=t_r[t_r[x].l].s_z+t_r[t_r[x].r].s_z+1;//左右子树大小的和 +1 
}
void p_d(int x){
    if(t_r[x].l_n){//释放懒标记 
        swap(t_r[x].l,t_r[x].r);//交换左右子树 
        t_r[t_r[x].l].l_n^=1;//懒标记下传 
		t_r[t_r[x].r].l_n^=1;
		t_r[x].l_n=0;//清空懒标记 
    }
}
void b_l(int x,int y,int z){//左旋 
	//旋转(从根节点向上转) 
	t_r[y].r=t_r[x].l;
	t_r[t_r[y].r].f=y;
    t_r[x].l=y;
	t_r[y].f=x;//更新父节点 
	t_r[x].f=z;//更新父节点 
    u_p(x);//更新节点信息 
	u_p(y);
}
void b_r(int x,int y,int z){//右旋 
	//旋转(从根节点向上转) 
	t_r[y].l=t_r[x].r;
	t_r[t_r[y].l].f=y;
    t_r[x].r=y;
	t_r[y].f=x;//更新父节点 
	t_r[x].f=z;//更新父节点 
    u_p(x);//更新节点信息 
	u_p(y);
}
void l_r(int x,int &k){
    int y=t_r[x].f;//原点父节点 
	int z=t_r[y].f;//原点的父节点的父节点 
	if(y==k)k=x;//父节点即目标状态 
	else{//爷节点即目标状态 
		if(t_r[z].l==y)t_r[z].l=x;//都是左儿子 
		else t_r[z].r=x;//都是右儿子 
	}
    if(t_r[y].l==x)b_r(x,y,z);//右旋 
	else b_l(x,y,z);//左旋 
}
void x_t(int x,int &k){
    while(x!=k){//没有到达预期位置 
        int y=t_r[x].f;//原点的父节点 
		int z=t_r[y].f;//原节点的父节点的父节点
        if(y!=k){//父节点也没有到达预期 
            if((t_r[y].l==x)^(t_r[z].l==y))l_r(x,k);//一条链(都是左儿子或右儿子) 
            else l_r(y,k);//不是一条链 
        }
        l_r(x,k);//父节点到达预期,只用再旋转一次即可到达目的 
    }
}
void b_t(int l,int r,int f){
    if(l>r)return;//不在区间内,退出 
    int m_i=(l+r)>>1;
    if(m_i<f)t_r[f].l=m_i;//符合平衡树特性,存储左右节点 
	else t_r[f].r=m_i;
    t_r[m_i].f=f;//存父节点 
	t_r[m_i].s_z=1;//初始化节点大小 
    if(l==r)return;//子节点,不会再存在左右子树,退出 
    b_t(l,m_i-1,m_i);//左子树 
	b_t(m_i+1,r,m_i);//右子树 
    u_p(m_i);//更新当前节点 
}
int g_a(int x,int k){
    p_d(x);//释放懒标记 
	int s=t_r[t_r[x].l].s_z;//左子树大小 
    if(k==s+1)return x;//左子树大小 +1 即本点的排名 
    if(k<=s)return g_a(t_r[x].l,k);//被左子树包含 
    else return g_a(t_r[x].r,k-s-1);//被右子树包含 
}
void x_z(int l,int r){
    int x=g_a(g_g,l);//在树上找到 x 的坐标 
	int y=g_a(g_g,r+2);//再树上找到 y 的坐标 
    x_t(x,g_g);//将 x 换到根节点 
	x_t(y,t_r[x].r);//将 y 换到根节点右儿子节点 
	int z=t_r[y].l;//打懒标记 
    t_r[z].l_n^=1;
}
int main(){
    n=r_r(),m=r_r();
    g_g=(n+1)/2+1;//定义根 
	b_t(1,n+2,g_g);//建树(初始化) 
    for(int i=1;i<=m;i++){
        int l=r_r(),r=r_r();
        x_z(l,r);//旋转区间 
    }
    for(int i=2;i<=n+1;i++)printf("%d ",g_a(g_g,i)-1);//输出每个数 
    return 0;
}

文章作者: 王大神——A001
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王大神——A001 !
  目录