用序列之王splay解决线段树经典问题

13 篇文章 0 订阅

我们尝试用序列之王spaly来解决线段树经典两个问题:
最大值和最大值2。

最大值

 在N(1<=N<=100000)个数A1…An组成的序列上进行M(1<=M<=100000)次操作,操作有两种:
  1 x y:表示修改A[x]为y;
  2 x y:询问x到y之间的最大值。

这是线段树很容易解决的问题。
我们现在考虑用splay解决。
我们设key[i]表示结点i的值。
那么初始情况下key[i]=a[i]。
设num[i]表示以结点i为根的子树中的最大值。
我们将n个结点弄成一条链。
然后旋最小的至根进行平衡调整。
接下来我们可以打出所有splay基本操作:
pd(x):判断x是否为其父亲的右子树。
updata(x):更新结点x的值(例如num就需要更新)。
rotate(x):将结点x往上旋(需要用到函数pd)。
splay(x,y):将结点x旋至结点y下方(如果y=0相当于旋到顶)。
kth(x,y):在以结点x为根的子树中查找第y小的结点。
split(x,y,l,r):在以结点x为根的子树中(注意father[x]为0),从第y小的元素后分开。让前y个元素变为一棵splay,其余变为一棵splay,其中前者根节点赋值给l,后者根节点赋值给r。
merge(l,r,x):将以结点l为根的splay和以结点r为根的splay合并成一棵splay(father[l]=0,father[r]=0),得到的新splay根节点赋值给x。

为了支持kth,merge操作,我们需要维护size[i]表示以i为根的子树中有多少结点。
为了更好更优美的执行split和merge操作[即使分裂后两棵splay均非空],我们在第一个元素添加虚拟元素[虚拟结点]n+1,在最后一个元素后添加虚拟元素[虚拟结点]n+2。
这些操作的实现不讲,不懂的可以去学习然后看一下参考程序的实现。
注意的是虚拟结点和空结点的num值和key值都得赋值为-inf。

参考程序

#include<cstdio>
#include<algorithm>
#include<climits>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define maxn (100000+5)
#define inf (INT_MAX)
using namespace std;
int tree[maxn][3],father[maxn],key[maxn],num[maxn],size[maxn];
int i,j,k,l,r,mid,t,n,m,tot,root,x,y;
int pd(int x){
    if (tree[father[x]][0]==x) return 0;else return 1;
}
void updata(int x){
    num[x]=max(key[x],max(num[tree[x][0]],num[tree[x][1]]));
    size[x]=size[tree[x][0]]+size[tree[x][1]]+1;
}
void rotate(int x){
    int y=father[x],z=pd(x);
    tree[y][z]=tree[x][1-z];
    if (tree[x][1-z]) father[tree[y][z]]=y;
    father[x]=father[y];
    if (father[y]) tree[father[y]][pd(y)]=x;
    father[y]=x;
    tree[x][1-z]=y;
    updata(y);
    updata(x);
}
void splay(int x,int y){
    while (father[x]!=y){
        if (father[father[x]]!=y)
            if (pd(x)==pd(father[x])) rotate(x);else rotate(father[x]);
        rotate(x);
    }
}
int kth(int x,int y){
    if (size[tree[x][0]]+1==y) return x;
    else if (size[tree[x][0]]+1>y) return kth(tree[x][0],y);
    else return kth(tree[x][1],y-size[tree[x][0]]-1);
}
void split(int x,int y,int &l,int &r){
    int j=kth(x,y);
    splay(j,0);
    int k=tree[j][1];
    tree[j][1]=0;
    father[k]=0;
    l=j;
    r=k;
    updata(j);
}
void merge(int l,int r,int &x){
    int j=kth(l,size[l]);
    splay(j,0);
    tree[j][1]=r;
    father[r]=j;
    x=j;
    updata(j);
}
int main(){
    scanf("%d",&n);
    key[0]=key[n+2]=key[n+1]=-inf;
    num[n+1]=num[0]=-inf;
    size[0]=0;
    size[n+1]=size[n+2]=1;
    fo(i,1,n){
        scanf("%d",&key[i]);
        if (i>1){
            tree[i][0]=i-1;
            father[i-1]=i;
            updata(i);
        }
        else{
            tree[1][0]=n+1;
            father[n+1]=1;
            updata(1);
        }
    }
    tree[n+2][0]=n;
    father[n]=n+2;
    updata(n+2);
    root=n+2;
    splay(n+1,0);
    root=n+1;
    scanf("%d",&m);
    fo(i,1,m){
        scanf("%d%d%d",&t,&x,&y);
        if (t==1){
            splay(x,0);
            root=x;
            key[x]=y;
            updata(x);
        }
        else{
            split(root,y+1,l,r);
            split(l,x,l,mid);
            printf("%d\n",num[mid]);
            merge(l,mid,l);
            merge(l,r,root);
        }
    }
}

最大值2

这次要求我们支持区间修改(区间都加上一个数,可以是负数),怎么办?
我们学习线段树,可以打懒标记。
因此add[i]表示以i结点为根的splay全部加上add[i]。
当一个j需要进行splay操作时,沿途的结点就进行懒标记下传。
那么我们只需要在第一题的基础上加两个操作:
remove(x,y):在进行splay(x,y)时,对沿途的结点进行懒标记下传。
clear(x):对结点x进行懒标记下传。

然后我们发现并不能对,在答案为负数的情况下会输出0。

其实,是因为虚拟结点它是真实不存在的,因此你不能改变它的key值。而在虚拟结点没有左右子树时,也不能改变它的num值。
特殊处理一下即可。具体看程序实现。
为什么第一题不会出现这种情况?因为第一题不需要区间修改,就没有懒标记下传。虚拟结点的key永远不会被修改。

参考程序

#include<cstdio>
#include<algorithm>
#include<climits>
#include<stack>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define maxn (100000+5)
#define inf (INT_MAX)
using namespace std;
stack <int> s;
int tree[maxn][3],father[maxn],key[maxn],num[maxn],size[maxn],add[maxn];
int i,j,k,l,r,mid,t,n,m,tot,root,x,y;
int pd(int x){
    if (tree[father[x]][0]==x) return 0;else return 1;
}
void updata(int x){
    num[x]=max(key[x],max(num[tree[x][0]],num[tree[x][1]]));
    size[x]=size[tree[x][0]]+size[tree[x][1]]+1;
}
void rotate(int x){
    int y=father[x],z=pd(x);
    tree[y][z]=tree[x][1-z];
    if (tree[x][1-z]) father[tree[y][z]]=y;
    father[x]=father[y];
    if (father[y]) tree[father[y]][pd(y)]=x;
    father[y]=x;
    tree[x][1-z]=y;
    updata(y);
    updata(x);
}
void clear(int x){
    if (add[x]){
        if (tree[x][0]){
            if (tree[x][0]<=n||(tree[x][0]>n&&size[tree[x][0]]>1))
                num[tree[x][0]]+=add[x];
            if (tree[x][0]<=n)
                key[tree[x][0]]+=add[x];
            add[tree[x][0]]+=add[x];
        }
        if (tree[x][1]){
            if (tree[x][1]<=n||(tree[x][1]>n&&size[tree[x][1]]>1))
                num[tree[x][1]]+=add[x];
            if (tree[x][1]<=n)
                key[tree[x][1]]+=add[x];
            add[tree[x][1]]+=add[x];
        }
        add[x]=0;
    }
}
void remove(int x,int y){
    do{
        s.push(x);
        x=father[x];
    }while (x!=y);
    while (!s.empty()){
        x=s.top();
        clear(x);
        s.pop();
    }
}
void splay(int x,int y){
    remove(x,y);
    while (father[x]!=y){
        if (father[father[x]]!=y)
            if (pd(x)==pd(father[x])) rotate(x);else rotate(father[x]);
        rotate(x);
    }
}
int kth(int x,int y){
    if (size[tree[x][0]]+1==y) return x;
    else if (size[tree[x][0]]+1>y) return kth(tree[x][0],y);
    else return kth(tree[x][1],y-size[tree[x][0]]-1);
}
void split(int x,int y,int &l,int &r){
    int j=kth(x,y);
    splay(j,0);
    int k=tree[j][1];
    tree[j][1]=0;
    father[k]=0;
    l=j;
    r=k;
    updata(j);
}
void merge(int l,int r,int &x){
    int j=kth(l,size[l]);
    splay(j,0);
    tree[j][1]=r;
    father[r]=j;
    x=j;
    updata(j);
}
int main(){
    scanf("%d",&n);
    key[0]=key[n+2]=key[n+1]=-inf;
    num[n+1]=num[0]=-inf;
    size[0]=0;
    size[n+1]=size[n+2]=1;
    fo(i,1,n){
        scanf("%d",&key[i]);
        if (i>1){
            tree[i][0]=i-1;
            father[i-1]=i;
            updata(i);
        }
        else{
            tree[1][0]=n+1;
            father[n+1]=1;
            updata(1);
        }
    }
    tree[n+2][0]=n;
    father[n]=n+2;
    updata(n+2);
    root=n+2;
    splay(n+1,0);
    root=n+1;
    scanf("%d",&m);
    fo(i,1,m){
        scanf("%d%d%d",&t,&x,&y);
        if (t==1){
            scanf("%d",&t);
            split(root,y+1,l,r);
            split(l,x,l,mid);
            num[mid]+=t;
            key[mid]+=t;
            add[mid]+=t;
            merge(l,mid,l);
            merge(l,r,root);
        }
        else{
            split(root,y+1,l,r);
            split(l,x,l,mid);
            printf("%d\n",num[mid]);
            merge(l,mid,l);
            merge(l,r,root);
        }
    }
}

拓展延伸

实际上splay还可以做很多线段树做不了的东西。
例如翻转操作(排序机械臂)。
可以维护连通性(网络通信)。
序列之王,维护序列(维护序列是一道题目)。
大家可以去做。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值