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

 

 

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

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

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

P2042 [NOI2005]维护数列

题目描述

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

输入输出格式

输入格式:spa

 

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

 

输出格式:code

 

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

 

输入输出样例

输入样例#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 个数。blog

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

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\n",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\n",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 }
相关文章
相关标签/搜索