• 洛谷 P2042 [NOI2005]维护数列-Splay(插入 删除 修改 翻转 求和 最大的子序列)


    因为要讲座,随便写一下,等讲完有时间好好写一篇splay的博客。

    先直接上题目然后贴代码,具体讲解都写代码里了。

    参考的博客等的链接都贴代码里了,有空再好好写。

    P2042 [NOI2005]维护数列

    题目描述

    请写一个程序,要求维护一个数列,支持以下 6 种操作:(请注意,格式栏 中的下划线‘ _ ’表示实际输入文件中的空格)

    输入输出格式

    输入格式:

    输入文件的第 1 行包含两个数 N 和 M,N 表示初始时数列中数的个数,M 表示要进行的操作数目。 第 2 行包含 N 个数字,描述初始时的数列。 以下 M 行,每行一条命令,格式参见问题描述中的表格

    输出格式:

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

    输入输出样例

    输入样例#1: 复制
    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
    输出样例#1: 复制
    -1
    10
    1
    10

    说明

    你可以认为在任何时刻,数列中至少有 1 个数。

    输入数据一定是正确的,即指定位置的数在数列中一定存在。

    50%的数据中,任何时刻数列中最多含有 30 000 个数;

    100%的数据中,任何时刻数列中最多含有 500 000 个数。

    100%的数据中,任何时刻数列中任何一个数字均在[-1 000, 1 000]内。

    100%的数据中,M ≤20 000,插入的数字总数不超过 4 000 000 。

    代码:

      1 /*
      2 https://www.luogu.org/problemnew/show/P2042
      3 https://www.luogu.org/problemnew/solution/P2042
      4 https://baijiahao.baidu.com/s?id=1613228134219334653&wfr=spider&for=pc
      5 https://www.cnblogs.com/victorique/p/8478866.html
      6 https://www.cnblogs.com/noip/archive/2013/05/31/3111169.html
      7 https://baike.baidu.com/item/%E4%BC%B8%E5%B1%95%E6%A0%91/7003945?fr=aladdin
      8 https://blog.csdn.net/changtao381/article/details/8936765
      9 https://blog.csdn.net/huzujun/article/details/81394092
     10 */
     11 
     12 //插入 删除 修改 翻转 求和 最大的子序列
     13 #include<bits/stdc++.h>
     14 using namespace std;
     15 typedef long long ll;
     16 const int maxn=1e6+10;
     17 const int inf=0x3f3f3f3f;
     18 
     19 int n,m,rt,cnt;
     20 int a[maxn],id[maxn],fa[maxn],tree[maxn][2];
     21 int sum[maxn],sz[maxn],val[maxn],mx[maxn],lx[maxn],rx[maxn];
     22 int tag[maxn],rev[maxn];
     23 //tag 是否有统一修改的标记,rev 是否有统一翻转的标记
     24 
     25 queue<int> q;
     26 
     27 void pushup(int x)//分治,类似线段树的区间合并,但是因为当前节点也有值,所以要加上当前节点的val
     28 {
     29     int l=tree[x][0],r=tree[x][1];
     30     sum[x]=sum[l]+sum[r]+val[x];
     31     sz[x]=sz[l]+sz[r]+1;
     32     lx[x]=max(lx[l],sum[l]+lx[r]+val[x]);
     33     rx[x]=max(rx[r],sum[r]+rx[l]+val[x]);
     34     mx[x]=max(max(mx[l],mx[r]),rx[l]+lx[r]+val[x]);//区间最大子段和
     35 }
     36 
     37 void pushdown(int x)
     38 {
     39     int l=tree[x][0],r=tree[x][1];
     40     if(tag[x]){//有统一修改的标记,翻转就没有意义了
     41         rev[x]=tag[x]=0;
     42         if(l) tag[l]=1,val[l]=val[x],sum[l]=sz[l]*val[x];
     43         if(r) tag[r]=1,val[r]=val[x],sum[r]=sz[r]*val[x];
     44         if(val[x]>=0){
     45             if(l) lx[l]=rx[l]=mx[l]=sum[l];
     46             if(r) lx[r]=rx[r]=mx[r]=sum[r];
     47         }
     48         else{
     49             if(l) lx[l]=rx[l]=0,mx[l]=val[x];
     50             if(r) lx[r]=rx[r]=0,mx[r]=val[x];
     51         }
     52     }
     53     if(rev[x]){
     54         rev[x]=0;rev[l]^=1;rev[r]^=1;
     55         swap(lx[l],rx[l]);swap(lx[r],rx[r]);//注意,在翻转操作中,前后缀的最长上升子序列都反过来了,很容易错
     56         swap(tree[l][0],tree[l][1]);swap(tree[r][0],tree[r][1]);
     57     }
     58 }
     59 
     60 void rotate(int x,int &k)
     61 {
     62     int y=fa[x],z=fa[y],l=(tree[y][1]==x),r=l^1;
     63     if(y==k) k=x;
     64     else tree[z][tree[z][1]==y]=x;
     65     fa[tree[x][r]]=y;fa[y]=x;fa[x]=z;//改变父子关系。爸爸变儿子,爷爷变爸爸
     66     tree[y][l]=tree[x][r];tree[x][r]=y;
     67     pushup(y);pushup(x);//旋转操作,改变关系之后标记上传
     68 }
     69 
     70 /*
     71 伸展操作,三种状态:
     72 1.x的爸爸y是目标状态,直接翻转x
     73 2.x有爸爸y,有爷爷z,如果三点在一条直线上,就先翻转爸爸y,这样翻转是双旋,保持平衡(关于旋转 双旋、单旋,讲一下)
     74 3.x有爸爸y,有爷爷z,三点不在一条直线上,直接翻转两次x就可以
     75 */
     76 void splay(int x,int &k)//伸展操作,核心操作
     77 {
     78     while(x!=k){//一直到转到目标状态
     79         int y=fa[x],z=fa[y];
     80         if(y!=k){//如果爸爸不是目标状态
     81             if((tree[z][0]==y)^(tree[y][0]==x)) rotate(x,k);//如果三点不在一条直线上,直接转自己
     82             else rotate(y,k);
     83         }
     84         rotate(x,k);
     85     }
     86 }
     87 
     88 /*
     89 查找操作,核心操作之二
     90 区间翻转和插入以及删除的操作都需要find操作
     91 因为维护的区间的实际编号是不连续的,所以需要查找要操作的区间对应平衡树的中序遍历的那段区间
     92 */
     93 int find(int x,int rk)//找排名第rk的
     94 {
     95     pushdown(x);//因为所有操作都是需要find,所以在这里标记下传就可以
     96     int l=tree[x][0],r=tree[x][1];
     97     if(sz[l]+1==rk) return x;//就是二叉树的搜索操作
     98     if(sz[l]  >=rk) return find(l,rk);
     99     else return find(r,rk-sz[l]-1);
    100 
    101 }
    102 
    103 /*
    104 这道题极限是4*10^6*log(2*10^4),2为底,二分,所以4*10^10,128MB差不多存10^8,爆内存
    105 用时间换空间的回收冗余编号机制
    106 */
    107 void recycle(int x)//垃圾回收,节省内存,因为内存开销太大,容易爆内存,记录用过但是已经删除的节点的编号,新建节点的时候直接从队列或者栈中取出来用就可以。时间换空间
    108 {
    109     int &l=tree[x][0],&r=tree[x][1];
    110     if(l) recycle(l);
    111     if(r) recycle(r);//垃圾回收,一直回收到底
    112     q.push(x);
    113     fa[x]=tag[x]=rev[x]=l=r=0;
    114 }
    115 
    116 /*
    117 核心操作之三
    118 通过split 找到[k+1,k+tot],然后把k,k+tot+1移到根和右儿子的位置
    119 然后返回这个右儿子的左儿子,就是要操作的区间
    120 */
    121 int split(int k,int tot)
    122 {
    123     int x=find(rt,k),y=find(rt,k+tot+1);
    124     splay(x,rt);splay(y,tree[x][1]);
    125     return tree[y][0];
    126 }
    127 
    128 void query(int k,int tot)//区间最大子段和
    129 {
    130     int x=split(k,tot);
    131     printf("%d
    ",sum[x]);
    132 }
    133 
    134 void modify(int k,int tot,int value)//当前数列第k个开始连续tot个统一修改为value
    135 {
    136     int x=split(k,tot),y=fa[x];
    137     val[x]=value;tag[x]=1;sum[x]=sz[x]*value;
    138     if(value>=0) lx[x]=rx[x]=mx[x]=sum[x];
    139     else         lx[x]=rx[x]=0,mx[x]=value;//最大的子段和就是一个,因为是负数
    140     pushup(y);pushup(fa[y]);//每一步的修改操作,父子关系发生变化,记录标记发生变化,所以要及时标记上传
    141 }
    142 
    143 void rever(int k,int tot)//当前数列第k个开始的tot个数字翻转
    144 {
    145     int x=split(k,tot),y=fa[x];
    146     if(!tag[x]){
    147         rev[x]^=1;
    148         swap(tree[x][0],tree[x][1]);
    149         swap(lx[x],rx[x]);
    150         pushup(y);pushup(fa[y]);
    151     }
    152 }
    153 
    154 void erase(int k,int tot)//当前数列第k个数字开始连续删除tot个数字
    155 {
    156     int x=split(k,tot),y=fa[x];
    157     recycle(x);tree[y][0]=0;
    158     pushup(y);pushup(fa[y]);
    159 }
    160 
    161 void build(int l,int r,int f)
    162 {
    163     int m=(l+r)>>1,now=id[m],pre=id[f];
    164     if(l==r){
    165         mx[now]=sum[now]=a[l];
    166         tag[now]=rev[now]=0;//这里的清零操作是必要的,因为可能是之前垃圾回收,冗余的
    167         lx[now]=rx[now]=max(a[l],0);
    168         sz[now]=1;
    169     }
    170     if(l<m) build(l,m-1,m);
    171     if(r>m) build(m+1,r,m);
    172     val[now]=a[m];fa[now]=pre;
    173     pushup(now);
    174     tree[pre][m>=f]=now;//插入右或者左区间
    175 }
    176 
    177 void insert(int k,int tot)//当前数列第k个数字后插入tot个数字
    178 {
    179     for(int i=1;i<=tot;i++){
    180         scanf("%d",&a[i]);
    181     }
    182     for(int i=1;i<=tot;i++){
    183         if(!q.empty()) id[i]=q.front(),q.pop();
    184         else id[i]=++cnt;//利用队列里的冗余节点编号
    185     }
    186     build(1,tot,0);//将读入的tot个数建成一个平衡树
    187     int z=id[(1+tot)>>1];//取中点为根
    188     int x=find(rt,k+1),y=find(rt,k+2);//首先,根据中序遍历,找到要操作的区间的实际编号
    189     splay(x,rt);splay(y,tree[x][1]);//把k+1(注意我们已经右移了一个单位)和(k+1)+1移到根和右儿子
    190     fa[z]=y;tree[y][0]=z;//直接把需要插入的这个平衡树挂到右儿子的左儿子上去就好了
    191     pushup(y);pushup(x);
    192 }
    193 
    194 //对于具体在哪里上传标记和下传标记
    195 //可以这么记,只要用了split就要重新上传标记
    196 //只有find中需要下传标记
    197 //但其实,你多传几次是没有关系的,但是少传了就不行了
    198 int main()
    199 {
    200     scanf("%d%d",&n,&m);
    201     mx[0]=a[1]=a[n+2]=-inf;//两个虚拟节点,哨兵节点
    202     for(int i=1;i<=n;i++){
    203         scanf("%d",&a[i+1]);
    204     }
    205     for(int i=1;i<=n+2;i++){//虚拟了两个节点1和n+2,然后把需要操作区间整体右移一个单位
    206         id[i]=i;
    207     }
    208     build(1,n+2,0);
    209     rt=(n+3)>>1;cnt=n+2;//取最中间的为根,这样就是一个完美的平衡树
    210     int k,tot,value;char op[10];
    211     for(int i=1;i<=m;i++){
    212         scanf("%s",op);
    213         if(op[0]!='M'||op[2]!='X') scanf("%d%d",&k,&tot);
    214         if(op[0]=='I') insert(k,tot);
    215         if(op[0]=='D') erase(k,tot);
    216         if(op[0]=='M'){
    217             if(op[2]=='X') printf("%d
    ",mx[rt]);
    218             else scanf("%d",&value),modify(k,tot,value);
    219         }
    220         if(op[0]=='R') rever(k,tot);
    221         if(op[0]=='G') query(k,tot);
    222     }
    223     return 0;
    224 }
  • 相关阅读:
    【转】最奇特的编程语言特征
    【原】mysql 视图
    自己动手写ORM框架(一):目标效果预览
    (二)Javascript面向对象:命名空间
    在JAVA中封装JSONUtils工具类及使用(一)
    .NET3.5中JSON用法以及封装JsonUtils工具类(二)
    (四)Javascript面向对象:继承
    自己动手写ORM框架(三):关系映射配置—Table属性
    (一)javascript面向对象:(2)类
    自己动手写ORM框架(二):AdoHelper支持多数据库操作的封装(1)
  • 原文地址:https://www.cnblogs.com/ZERO-/p/10733566.html
Copyright © 2020-2023  润新知