洛谷P2042 [NOI2005] 维护数列
洛谷P2042 [NOI2005] 维护数列
洛谷题目传送门
题目描述
请写一个程序,要求维护一个数列,支持以下 666 种操作:
编号 | 名称 | 格式 | 说明 |
---|---|---|---|
1 | 插入 | INSERTpositotc1c2⋯ctot\operatorname{INSERT}\ posi \ tot \ c_1 \ c_2 \cdots c_{tot}INSERT posi tot c1 c2⋯ctot | 在当前数列的第 posiposiposi 个数字后插入 tottottot 个数字:c1,c2⋯ctotc_1, c_2 \cdots c_{tot}c1,c2⋯ctot;若在数列首插入,则 posiposiposi 为 000 |
2 | 删除 | DELETEpositot\operatorname{DELETE} \ posi \ totDELETE posi tot | 从当前数列的第 posiposiposi 个数字开始连续删除 tottottot 个数字 |
3 | 修改 | MAKE-SAMEpositotc\operatorname{MAKE-SAME} \ posi \ tot \ cMAKE-SAME posi tot c | 从当前数列的第 posiposiposi 个数字开始的连续 tottottot 个数字统一修改为 ccc |
4 | 翻转 | REVERSEpositot\operatorname{REVERSE} \ posi \ totREVERSE posi tot | 取出从当前数列的第 posiposiposi 个数字开始的 tottottot 个数字,翻转后放入原来的位置 |
5 | 求和 | GET-SUMpositot\operatorname{GET-SUM} \ posi \ totGET-SUM posi tot | 计算从当前数列的第 posiposiposi 个数字开始的 tottottot 个数字的和并输出 |
6 | 求最大子列和 | MAX-SUM\operatorname{MAX-SUM}MAX-SUM | 求出当前数列中和最大的一段子列,并输出最大和 |
输入格式
第一行包含两个整数 NNN 和 MMM,NNN 表示初始时数列中数的个数,MMM 表示要进行的操作数目。
第二行包含 NNN 个数字,描述初始时的数列。以下 MMM 行,每行一条命令,格式参见问题描述中的表格。
输出格式
对于输入数据中的 GET-SUM\operatorname{GET-SUM}GET-SUM 和 MAX-SUM\operatorname{MAX-SUM}MAX-SUM 操作,向输出文件依次打印结果,每个答案(数字)占一行。
输入输出样例 #1
输入 #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
说明/提示
数据规模与约定
- 你可以认为在任何时刻,数列中至少有 111 个数。
- 输入数据一定是正确的,即指定位置的数在数列中一定存在。
- 对于 50%50\%50% 的数据,任何时刻数列中最多含有 3×1043 \times 10^43×104 个数。
- 对于 100%100\%100% 的数据,任何时刻数列中最多含有 5×1055 \times 10^55×105 个数,任何时刻数列中任何一个数字均在 [−103,103][-10^3, 10^3][−103,103] 内,1≤M≤2×1041 \le M \le 2 \times 10^41≤M≤2×104,插入的数字总数不超过 4×1064 \times 10^64×106。
题面由 @syksykCCC 提供。
思路详解
确定算法
根据我们的目测,这显然是一个裸数据结构的题,由于他要区间翻转,我们很容易就想到了要使用平衡树,不会区间翻转的可以去看这道题。在这里我采用的是无旋平衡树(FHQ)。下面我将为你相信讲解这道题。
一切的根基
由于我采用的是指针,我们首先要定义好结构体,并做好安全访问的每一个元素的函数,具体如下:
struct lit{int val,sum,pri,siz;
//值,区间和,优先级,大小int pre,suf,sub;
//最大前缀,最大后缀,最大子序列int co,rev;
//染色,翻转的懒标记lit *l,*r;
//左右子节点lit(int v){//构造val=sum=sub=v;//由于要求必须有一个元素,所以sub=v(后面会解释pre与suf)pri=rand();siz=1;pre=suf=max(0,v);co=-0x3f3f3f3f;rev=0;l=r=nullptr;}
};
//............................
int getsum(lit *u){return u?u->sum:0;}//安全访问
int getsize(lit *u){return u?u->siz:0;}
int getsub(lit *u){return u?u->sub:-0x3f3f3f3f;}
int getpre(lit *u){return u?u->pre:0;}
int getsuf(lit *u){return u?u->suf:0;}
打标记,更新与下传
我们要进行两种标记:
- 区间染色:染色了再翻转就没意义了,我们可以进行一下优化,染色后sum,pre,sub,suf,valsum,pre,sub,suf,valsum,pre,sub,suf,val都要改变,与构造差不多。
- 区间翻转:翻转首先要交换左右子树,然后,pre,sufpre,sufpre,suf也要交换。
void mark_co(lit *&u,int v){//区间染色if(!u)return;u->co=v;//更新标记u->sum=u->siz*v;//更新区间和u->val=v;//更新单点的值u->pre=u->suf=max(u->sum,0);//更新pre,suf,同上u->sub=max(v,u->sum);//sub至少一个元素,所以要么只取一个,要么全部取u->rev=0;//不需要翻转了}void mark_rev(lit *&u){//区间翻转if(!u)return;u->rev^=1;//更新标记swap(u->l,u->r);//交换左右子树swap(u->pre,u->suf);//交换pre,suf}
标记下传很简单,依次下传2个标记即可。
void pushdown(lit *&u){if(!u)return ;if(u->co!=-0x3f3f3f3f){//下传染色标记mark_co(u->l,u->co);mark_co(u->r,u->co);u->co=-0x3f3f3f3f;}if(u->rev){//下传翻转标记mark_rev(u->l);mark_rev(u->r);u->rev=0;}}
下面,到了本题比较难的部分了,那就是更新。更新的其他的值都没什么好说的,但我们要思考如何更新pre,suf,subpre,suf,subpre,suf,sub。我会在下面代码中详细讲解:
void pushup(lit *&u){if(!u)return;u->siz=1+getsize(u->l)+getsize(u->r);u->sum=getsum(u->l)+u->val+getsum(u->r);u->pre=max(max(getpre(u->l),getsum(u->l)+u->val+getpre(u->r)),0);
//最大前缀可能包含当前节点,也可能不。
//不包括时为getpre(u->l),包括时为getsum(u->l)+u->val+getpre(u->r)。u->suf=max(max(getsuf(u->r),getsum(u->r)+u->val+getsuf(u->l)),0);//同上u->sub=max(getsuf(u->l)+getpre(u->r)+u->val,max(getsub(u->l),getsub(u->r)));
//同理sub也可以包括或不包括,不包括时为max(getsub(u->l),getsub(u->r)),包括时为getsuf(u->l)+getpre(u->r)+u->val
}
FHQ核心
FHQ核心只有2个函数,分别为splitsplitsplit(分裂)与mergemergemerge(合并)。
void split(lit *u,int k,lit *&x,lit *&y){//分类if(!u){x=y=nullptr;return;}pushdown(u);//记得下传!!!int ls=getsize(u->l);if(ls+1<=k){x=u;split(u->r,k-ls-1,x->r,y);pushup(x);}else {y=u;split(u->l,k,x,y->l);pushup(y);}
}
lit *merge(lit *x,lit *y){if(!x)return y;if(!y)return x;pushdown(x);pushdown(y);//记得下传!!!if(x->pri>y->pri){x->r=merge(x->r,y);pushup(x);return x;}else{y->l=merge(x,y->l);pushup(y);return y;}
}
快速建点与删除
为了更好的完成此题,我们还要完成两个操作:
- 由于他要直接插入一个区间,所以我们要想办法快速生成这个区间,再合并即可。考虑同线段树一样二分处理。这样对mergemergemerge的调用只有log(n)log(n)log(n)次。
- 为了释放空间,我们要将一个区间彻底删除。
lit *add(int l,int r){//二分建树if(l==r)return new lit(t[l]);//边界int mid=(l+r)/2;return merge(add(l,mid),add(mid+1,r));}void clear(lit *&u){//递归删除if(!u)return ;clear(u->l);clear(u->r);delete u;}
封装
封装好每一个操作,我们的这道题就差不多了。
void insert(int p,int n){//加入一个区间lit *x,*y;split(root,p,x,y);root=merge(merge(x,add(1,n)),y);
}
void remove(int le,int ri){//删除区间[le,ri]lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);if(mid)clear(mid);root=merge(l,r);
}
void change_color(int le,int ri,int v){//区间染色lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);mark_co(mid,v);root=merge(merge(l,mid),r);
}
void change_reverse(int le,int ri){//区间翻转lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);mark_rev(mid);root=merge(merge(l,mid),r);
}
int query_sum(int le,int ri){//求区间和lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);int res=getsum(mid);root=merge(l,merge(mid,r));return res;
}
int query_sub(){//求整个区间的最大子序列return getsub(root);
}
code
完整代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+5,INF=-0x3f3f3f3f;
struct lit{int val,sum,pri,siz;
//值,区间和,优先级,大小int pre,suf,sub;
//最大前缀,最大后缀,最大子序列int co,rev;
//染色,翻转的懒标记lit *l,*r;
//左右子节点lit(int v){//构造val=sum=sub=v;//由于要求必须有一个元素,所以sub=v(后面会解释pre与suf)pri=rand();siz=1;pre=suf=max(0,v);co=-0x3f3f3f3f;rev=0;l=r=nullptr;}
};
int t[N],n,m;
class FHQ_Treap{
private:lit *root;int getsum(lit *u){return u?u->sum:0;}//安全访问int getsize(lit *u){return u?u->siz:0;}int getsub(lit *u){return u?u->sub:-0x3f3f3f3f;}int getpre(lit *u){return u?u->pre:0;}int getsuf(lit *u){return u?u->suf:0;}void mark_co(lit *&u,int v){//区间染色if(!u)return;u->co=v;//更新标记u->sum=u->siz*v;//更新区间和u->val=v;//更新单点的值u->pre=u->suf=max(u->sum,0);//更新pre,suf,同上u->sub=max(v,u->sum);//sub至少一个元素,所以要么只取一个,要么全部取u->rev=0;//不需要翻转了}void mark_rev(lit *&u){//区间翻转if(!u)return;u->rev^=1;//更新标记swap(u->l,u->r);//交换左右子树swap(u->pre,u->suf);//交换pre,suf}void pushdown(lit *&u){if(!u)return ;if(u->co!=-0x3f3f3f3f){//下传染色标记mark_co(u->l,u->co);mark_co(u->r,u->co);u->co=-0x3f3f3f3f;}if(u->rev){//下传翻转标记mark_rev(u->l);mark_rev(u->r);u->rev=0;}}void pushup(lit *&u){if(!u)return;u->siz=1+getsize(u->l)+getsize(u->r);u->sum=getsum(u->l)+u->val+getsum(u->r);u->pre=max(max(getpre(u->l),getsum(u->l)+u->val+getpre(u->r)),0);
//最大前缀可能包含当前节点,也可能不。
//不包括时为getpre(u->l),包括时为getsum(u->l)+u->val+getpre(u->r)。u->suf=max(max(getsuf(u->r),getsum(u->r)+u->val+getsuf(u->l)),0);//同上u->sub=max(getsuf(u->l)+getpre(u->r)+u->val,max(getsub(u->l),getsub(u->r)));
//同理sub也可以包括或不包括,不包括时为max(getsub(u->l),getsub(u->r)),包括时为getsuf(u->l)+getpre(u->r)+u->val}void split(lit *u,int k,lit *&x,lit *&y){//分类if(!u){x=y=nullptr;return;}pushdown(u);//记得下传!!!int ls=getsize(u->l);if(ls+1<=k){x=u;split(u->r,k-ls-1,x->r,y);pushup(x);}else {y=u;split(u->l,k,x,y->l);pushup(y);}}lit *merge(lit *x,lit *y){if(!x)return y;if(!y)return x;pushdown(x);pushdown(y);//记得下传!!!if(x->pri>y->pri){x->r=merge(x->r,y);pushup(x);return x;}else{y->l=merge(x,y->l);pushup(y);return y;}}lit *add(int l,int r){//二分建树if(l==r)return new lit(t[l]);//边界int mid=(l+r)/2;return merge(add(l,mid),add(mid+1,r));}void clear(lit *&u){//递归删除if(!u)return ;clear(u->l);clear(u->r);delete u;}
public:void insert(int p,int n){//加入一个区间lit *x,*y;split(root,p,x,y);root=merge(merge(x,add(1,n)),y);}void remove(int le,int ri){//删除区间[le,ri]lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);if(mid)clear(mid);root=merge(l,r);}void change_color(int le,int ri,int v){//区间染色lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);mark_co(mid,v);root=merge(merge(l,mid),r);}void change_reverse(int le,int ri){//区间翻转lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);mark_rev(mid);root=merge(merge(l,mid),r);}int query_sum(int le,int ri){//求区间和lit *l,*mid,*r;split(root,ri,mid,r);split(mid,le-1,l,mid);int res=getsum(mid);root=merge(l,merge(mid,r));return res;}int query_sub(){//求整个区间的最大子序列return getsub(root); }
}tr;
int main(){ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1;i<=n;i++)cin>>t[i];tr.insert(0,n);for(int i=1;i<=m;i++){string s;int x,y,z;cin>>s;if(s=="INSERT"){cin>>x>>y;for(int j=1;j<=y;j++)cin>>t[j];tr.insert(x,y);}else if(s=="DELETE"){cin>>x>>y;tr.remove(x,x+y-1);}else if(s=="MAKE-SAME"){cin>>x>>y>>z;tr.change_color(x,x+y-1,z);}else if(s=="REVERSE"){cin>>x>>y;tr.change_reverse(x,x+y-1);}else if(s=="GET-SUM"){cin>>x>>y;cout<<tr.query_sum(x,x+y-1)<<'\n';}else{cout<<tr.query_sub()<<'\n';}}return 0;
}