bzoj1500 修改数列 区间splay树讲解

      仅以此篇blog记录下区间splay树需要说明的几个方面,以供日后二次学习时使用

1、splay树的旋转

      要知道普通splay树一字型和之字型旋转是怎么实现的

2、区间splay树的含义:

      区间splay树,就是用splay树来维护一个数列及其相关信息。

      首先要搞清楚这棵树是怎么组织的:splay树作为平衡树的一种,满足左小右大的性质,那么这里的“大”和“小”指的是什么的大和小呢?注意,这里指的是数列下标的大小,也就是说,区间splay是按照下标的大小把线性的数列变成了树型结构。这样一来,以一个点x为根中序遍历这个子树,得到的就是原数列的一个连续的子序列a[l , l + 1 , ... , r],如果以整棵树的根遍历的话得到的就原数列a[1 , 2 , ... , n]。

     知道了这棵树怎么组织的,下一步就要知道树上每个节点的含义:树上的每个节点要维护的信息有两类,第一类是这个节点对应的数列中的元素的信息,比如5号节点对应的是数列中的第7个数,即a[7];第二类是这个节点为根的子树对应的区间的信息,这个信息可以是这个区间的最大值,总和,甚至是这个区间构成的字符串的hash值(bzoj1014),举个例子,以5号节点为根的子树对应的区间是a[3, 4 , 5 , 6 , 7],那么maxv[5]就表示a[3] ~ a[7]中的最大值。

3、区间splay树的增删改查插入翻转

   实现将数列一个区间进行增删改查插入翻转的功能,在splay上实现的思想都是相似的,首先就是要找到这个要进行操作的区间:

  

   比如我要对a[a , a + 1 , ... , b]这个区间进行操作,首先我要把splay树翻成上图所示的样子,翻成这个样子以后可以知道,*所表示的那个子树,就是区间a[a , a + 1 , ... , b],所以说我的操作都是对节点b+1的左子树进行的。

    值得一提的是,我怎么才能在splay中找到a-1和b+1节点呢?方法是我对每个节点x维护一个size[x],表示x子树的大小为size[x],这样一来我就可以通过的函数find_k递归找到整棵树中对应数列中a[k]这个数的节点编号。

   最后,无论对区间a[l , ... , r]增删改查插入翻转,其实现时都按照下列步骤:

  ①找到a[l - 1]对应节点x,通过splay操作,翻到根上去;找到a[r + 1]对应节点y,通过splay操作,一直翻到y成为x的儿子;

  ②对y的左子节点进行相应的操作(增删改查等)

  ③使用update函数更新x、y、以及y的左子儿子

因此,在做题的时候,要抄板的地方有rotate函数、splay函数、find_k函数、build_tree函数、set_root函数,另外不要忘了一上来插入两个无实际意义的节点,然后在他们之间进行build_tree操作

     

1500: [NOI2005]维修数列

Time Limit: 10 Sec   Memory Limit: 64 MB
Submit: 13919   Solved: 4479
[ Submit][ Status][ Discuss]

Description

Input

输入的第1 行包含两个数N 和M(M ≤20 000),N 表示初始时数列中数的个数,M表示要进行的操作数目。
第2行包含N个数字,描述初始时的数列。
以下M行,每行一条命令,格式参见问题描述中的表格。
任何时刻数列中最多含有500 000个数,数列中任何一个数字均在[-1 000, 1 000]内。
插入的数字总数不超过4 000 000个,输入文件大小不超过20MBytes。

Output

对于输入数据中的GET-SUM和MAX-SUM操作,向输出文件依次打印结果,每个答案(数字)占一行。

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

#pragma warning(disable:4786)
#pragma comment(linker, "/STACK:102400000,102400000")
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<stack>
#include<queue>
#include<map>
#include<set>
#include<vector>
#include<cmath>
#include<string>
#include<sstream>
#include<bitset>
#define LL long long
#define FOR(i,f_start,f_end) for(int i=f_start;i<=f_end;++i)
#define mem(a,x) memset(a,x,sizeof(a))
#define lson l,m,x<<1
#define rson m+1,r,x<<1|1
using namespace std;
const int INF = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const double PI = acos(-1.0);
const double eps=1e-6;
const int maxn = 5e5 + 1000;
int fa[maxn] , ch[maxn][2] , w[maxn] , size[maxn] , lmax[maxn] , rmax[maxn] , ans[maxn] , sum[maxn];
//w[i]是节点i中数的大小,size[i]是节点i为根的子树的大小,lmax\rmax分别是节点i为根的子树对应的序列中从左端点开始
//的连续最大值,以及右端点开始的连续最大值,ans[i]是节点i为根的树对应序列的一段和最大的连续区间的和
bool rev[maxn] , tag[maxn];     //rev[i]是节点i为根子树是否被翻转,tag[i]是节点i为根子树是否被全部修改权值
int a[maxn] , st[maxn];     //st中存放回收的节点
char s[20];
int n , m , tot , root , top;       //root为伸展树的根,top为st的栈顶指针
int newnode()       //拿出一个新的节点
{
    int num;
    if(top)
        num = st[top--];
    else
        num = ++tot;
    ch[num][0] = ch[num][1] = fa[num] = 0;
    tag[num] = rev[num] = 0;
    size[num] = 1;
    sum[num] = w[num] = rmax[num] = lmax[num] = -INF;      //如果区间伸展树维护的是其他值,这里要酌情修改
    return num;
}
void update(int x)      //相当于线段树的pushup功能
{
    if(!x)      return ;
    size[x] = size[ch[x][0]] + size[ch[x][1]] + 1;
    sum[x] = sum[ch[x][0]] + sum[ch[x][1]] + w[x];
    lmax[x] = max(lmax[ch[x][0]] , sum[ch[x][0]] + w[x] + max(0 , lmax[ch[x][1]]));        //这些都要根据伸展树维护的内容酌情修改
    rmax[x] = max(rmax[ch[x][1]] , sum[ch[x][1]] + w[x] + max(0 , rmax[ch[x][0]]));
    ans[x] = max(max(ans[ch[x][0]] , ans[ch[x][1]]) , max(0 , rmax[ch[x][0]]) + w[x] + max(0 , lmax[ch[x][1]]));
}
void reverse(int x)    //将节点x为根子树对应的序列翻转
{
    if(!x)      return ;
    swap(lmax[x] , rmax[x]);
    swap(ch[x][0] , ch[x][1]);
    rev[x] ^= 1;
}
void replace(int x , int d)     //将节点x为根子树对应序列中所有数置为d
{
    if(!x)      return;
    w[x] = d;   sum[x] = d * size[x];
    lmax[x] = rmax[x] = ans[x] = max(d , d * size[x]);
    tag[x] = 1;     rev[x] = 0;
}
void push_down(int x)
{
    if(rev[x]){
        if(ch[x][0])        reverse(ch[x][0]);
        if(ch[x][1])        reverse(ch[x][1]);
        rev[x] = 0;
    }
    if(tag[x]){
        if(ch[x][0])        replace(ch[x][0] , w[x]);
        if(ch[x][1])         replace(ch[x][1] , w[x]);
        tag[x] = 0;
    }
}
int dir(int x)      //若节点x是其父亲节点的左子节点,返回0;是右子节点,返回1
{
    return x == ch[fa[x]][1];
}
void rotate(int x)
{
    int y , z , a , b , c;
    y = fa[x];      z = fa[y];      b = dir(x);     a = ch[x][!b];
    if(z == 0)
        root = x;
    else{
        c = dir(y); ch[z][c] = x;
    }
    fa[x] = z;  fa[y] = x;  ch[x][!b] = y;  ch[y][b] = a;
    if(a)       fa[a] = y;
    update(y);      update(x);      //因为节点x和节点y为根子树中的节点改变了,所以要update
}
void down(int x)     //将x到root的所有节点信息更新
{
    if(fa[x])       down(fa[x]);
    push_down(x);
}
void splay(int x , int i)      //将x翻成节点i的直接子节点
{
    down(x);            //现在要将x向上翻了,所以先把他到root的内容更新
    int y , z , b , c;
    while(fa[x] != i){
        y = fa[x];      z = fa[y];
        if(z == i)      rotate(x);
        else{
            b = dir(x);     c = dir(y);
            if(b ^ c){                                  //“之”字型
                rotate(x);  rotate(x);
            }
            else{                                       //“一”字型
                rotate(y);  rotate(x);
            }
        }
    }
}
int find_k(int x , int k)         //在节点x为根的树对应的序列中,找第k个元素对应的值
{
    push_down(x);           //find_k的同时正好随手push_down
    if(size[ch[x][0]] == k - 1)     return x;
    if(size[ch[x][0]] > k - 1)      return find_k(ch[x][0] , k);
    else    return find_k(ch[x][1] , k - size[ch[x][0]] - 1);
}
void build_tree(int l , int r , int tt)     //把一个序列a[l , l + 1 , ... , r]建成以节点tt为根的树
{
    int mid = l + (r - l) / 2;
    w[tt] = a[mid];
    if(l == r){
        sum[tt] = lmax[tt] = rmax[tt] = ans[tt] = w[tt] ;       size[tt] = 1;
        return;
    }
    if(l < mid){
        ch[tt][0] = newnode();      fa[ch[tt][0]] = tt;     build_tree(l , mid - 1 , ch[tt][0]);
    }
    if(mid < r){
        ch[tt][1] = newnode();      fa[ch[tt][1]] = tt;     build_tree(mid + 1 , r , ch[tt][1]);
    }
    update(tt);
}
void erase(int x)
{
    if(!x)      return ;
    st[++top] = x;
    if(ch[x][0])        erase(ch[x][0]);
    if(ch[x][1])        erase(ch[x][1]);
}
int Query(int l , int num)      //查询题目当前数列[l , l + 1 , ... , l + num - 1]的和
{
    int x = find_k(root , l);       splay(x , 0);      //为什么在root里找第l大数而不是l-1大?别忘了一开始插入了-INF节点
    int y = find_k(ch[x][1] , num + 1);     splay(y , x);
    return sum[ch[y][0]];
}
void Insert(int l , int num)
{
    for(int i = 1 ; i <= num ; i++){
        scanf("%d" , &a[i]);
    }
    int x = find_k(root , l + 1);       splay(x , 0);
    int y = find_k(ch[x][1] , 1);         splay(y , x);
    ch[y][0] = newnode();   fa[ch[y][0]] = y;
    build_tree(1 , num , ch[y][0]);
    update(y);      update(x);
}
void Delete(int l , int num)        //删除题目当前数列中的[l , l + 1 , ... , l + num - 1]
{
    int x = find_k(root , l);       splay(x , 0);
    int y = find_k(ch[x][1] , num + 1);     splay(y , x);
    erase(ch[y][0]);
    fa[ch[y][0]] = 0;       ch[y][0] = 0;
    update(y);  update(x);
}
void Reverse(int l , int num)
{
    int x = find_k(root , l);       splay(x , 0);
    int y = find_k(ch[x][1] , num + 1);     splay(y , x);
    reverse(ch[y][0]);
    update(y);  update(x);
}
void Replace(int l , int num , int d)
{
    int x = find_k(root , l);       splay(x , 0);
    int y =find_k(ch[x][1] , num + 1);      splay(y , x);
    replace(ch[y][0] , d);
    update(y);      update(x);
}
void set_root()
{
    top = 0;
    lmax[0] = rmax[0] = ans[0] = -INF;
    tot = 2;    root = 1;
    fa[1]= 0;   size[1] = 2;  ch[1][1] = 2; w[1] = sum[1] = lmax[1] = rmax[1] = -INF;     //插入两个无实际意义的节点,以防止使用上面函数时越界
    fa[2] = 1;  size[2] = 1;  w[2] = sum[2] = lmax[2] = rmax[2] = -INF;
}
int main()
{
    int p , tot , val;
    scanf("%d %d" , &n , &m);
    for(int i = 1 ; i <= n ; i++){
        scanf("%d" , &a[i]);
    }
    set_root();
    ch[2][0] = newnode();       fa[ch[2][0]] = 2;       //把初始序列插在两个占坑用的节点中间很关键
    build_tree(1 , n , ch[2][0]);
    update(2);      update(1);
    for(int _ = 0 ; _ < m ; _++){
        scanf("%s" , s);
        if(s[0] == 'I'){
            scanf("%d %d" , &p , &tot);
            Insert(p , tot);
        }
        else if(s[0] == 'D'){
            scanf("%d %d" , &p , &tot);
            Delete(p , tot);
        }
        else if(s[0] == 'M'){
            if(s[2] == 'K'){
                scanf("%d %d %d" , &p , &tot , &val);
                Replace(p , tot , val);
            }
            else{
                printf("%d\n" , ans[root]);
            }
        }
        else if(s[0] == 'R'){
            scanf("%d %d" , &p , &tot);
            Reverse(p , tot);
        }
        else if(s[0] == 'G'){
            scanf("%d %d" , &p , &tot);
            int res = Query(p , tot);
            printf("%d\n" , res);
        }
    }
    return 0;
}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值