NOI2005 维护数列 题解 (洛谷 P2042) splay
前言
语文老师布置了随笔的作业,要求每周两篇,题材字数不限。
我对着我贫瘠的人生沉思了一会儿,决定用题解蒙混过关。
正文
区间翻转,一眼 splay。
其实我平衡树只会 splay
定眼一看全是区间操作,惯例把区间前缀旋到根,把区间后缀旋到根的右儿子,然后对着它的左儿子快乐操作就好了。
#define work ch[ch[rt][1]][0]
并没有什么用,但是可以简化代码
因为要找区间前驱后继,所以要在头尾插入哨兵节点以防越界。
fa[2]=rt=1,ch[1][1]=num=2;
插好了
插入了头节点,所以后面所有的 posi 都应该 +1。
然后依次分析每个操作。
1.区间插入
在第 posi 个数之后插入一段序列,所以区间前缀就是 kth(posi),后缀是 kth(posi+1)。(kth 即求第 k 个数,返回节点编号)
区间建树采用递归。
用中序遍历的顺序处理,还可以省略中转数组,边读入边建树。
code
int build(int l,int r,int f){
if(l>r) return 0;
int mid=(l+r)>>1,u=++num;
ch[u][0]=build(l,mid-1,u);
val[u]=read(),fa[u]=f;
ch[u][1]=build(mid+1,r,u);
pushup(u);
return u;
}
void insert(){
work=build(1,m,ch[rt][1]);
pushup(ch[rt][1]),pushup(rt);
}
//在 main 函数中:
n=read()+1,m=read();
//n 即为 posi(变量名只是个代号qwq),m为区间长度
splay(kth(n)),splay(kth(n+1),rt);
insert();
对初始序列的建树也可以视作上面这样的区间建树。
2~5.区间修改、查询
这些操作针对的均是从 posi 开始的 tot 个数,即区间 [posi,posi+tot)。
所以它的前缀为 kth(posi-1),后缀为 kth(posi+tot)。
对于区间删除,我们只需要断掉根的右儿子与它左儿子的联系即可。
对于区间推平,我们将区间中每个节点的区间和修改为区间大小与修改值的积。
区间翻转,相当于交换区间中每个节点的左右儿子。我们先交换当前节点的左右儿子。
注意到这两个操作均需要下传标记,所以开两个懒标记数组,chg[] 记录区间被赋的值,tag[] 记录区间是否翻转。
注意到
任何时刻数列中任何一个数字均在 [-10^3, 10^3] 内
于是我们可以通过给 chg[] 赋一个不在此范围内的值 inf 来表示此节点未被赋值。
对于区间求和,直接输出区间和即可。
不要忘记将哨兵节点的 chg[] 也赋为 inf。
由于需要求和,哨兵节点的 val[] 应为 0。
code
void change(int x,int k){
chg[x]=val[x]=k,tag[x]=0;//区间推平后所有节点都一样,没有必要翻转
sum[x]=siz[x]*k;
}
void turn(int x){
swap(ch[x][0],ch[x][1]);
tag[x]^=1;
}
void pushdown(int x){//注:由于所有操作均基于 kth(),所以仅需在 kth 中进行 pushdown
if(chg[x]!=inf){
change(ch[x][0],chg[x]);
change(ch[x][1],chg[x]);
tag[x]=0,chg[x]=inf;
}
if(tag[x]){
turn(ch[x][0]);
turn(ch[x][1]);
tag[x]=0;
}
}
//在 main 函数中:
blabla..//初始化
for(char op[10];q--;){
scanf(" %s",op);
blabla..//操作6
n=read()+1,m=read();
blabla..//操作1
splay(kth(n-1)),splay(kth(n+m),rt);
switch(op[0]){
case 'G':printf("%d\n",sum[work]);continue;
//switch 不能匹配 continue,这里继续循环、进行下一个操作
case 'D':work=0;break;
case 'R':turn(work);break;
case 'M':change(work,read());
}
pushup(ch[rt][1]),pushup(rt);//不要忘记在区间修改后上传
}
6.最大子段和
对于静态区间,这个问题有 O(n) 的简单 dp,当然这与此题无关。
下面简述一下适用于本题的带修改 O(nlogn) 做法。
对于每段区间,记录这一区间的最大子段和 ms[],最大前缀和 ls[],最大后缀和 rs[]。
对于单节点区间,显然易得,三者均为 max(0,val[])。
其他节点归并求解。
最大子段和对应子段有三种情况,完全位于左或右儿子,或横跨两儿子。
最大前缀和对应前缀有两种情况,完全位于左儿子,或包含左儿子、延伸至右儿子。最大后缀和同理。
在 pushup 函数中一并处理即可。
需要注意的是,在区间修改时,三者也会被修改。
具体地说,区间推平时,三者均为 max(0,sum[]);
区间翻转时,交换前后缀和。
code
void pushup(int x){
int l=ch[x][0],r=ch[x][1];
siz[x]=siz[l]+siz[r]+1;
sum[x]=sum[l]+val[x]+sum[r];
ls[x]=max(ls[l],sum[l]+val[x]+ls[r]);
rs[x]=max(rs[r],sum[r]+val[x]+rs[l]);
ms[x]=max(max(ms[l],ms[r]),rs[l]+val[x]+ls[r]);
//由于三者均非负,所以可以简化掉一些具体的分类,仅讨论这几种
//若该节点无左或右儿子,则 l 或 r 为 0,对应的值为 0,在取 max 时不会造成影响
//所以对于叶子节点也无需特判
}
void change(int x,int k){
chg[x]=mx[x]=val[x]=k,tag[x]=0;
sum[x]=siz[x]*k;
ls[x]=rs[x]=ms[x]=max(0,sum[x]);
}
void turn(int x){
swap(ch[x][0],ch[x][1]);
swap(ls[x],rs[x]);
tag[x]^=1;
}
//在 main 函数中:
if(op[2]=='X'){printf("%d\n",ms[rt]);continue;}
到这里,我们写完了一份代码。
提交后可以获得
90pts WA on #3
的好成绩。
定眼一看报错信息:
Wrong Answer.wrong answer On line 79 column 1, read 0, expected -.
一个负数答案被求解为 0。
回顾上面求最大子段和的过程,可以发现,我们在处理每个区间时,可以选择不选,将答案记为 0。
这样,当区间内的数全部为负时,所得答案为 0。
如果要求最大子段和必须选数,则需要特判一下这种情况。
这里提供一种特判思路wtcl想不到其他的方法:
记录区间最大值 mx[],若最大值为负,则区间全部为负,此时显然最大子段和即为这个值。
否则输出求得的最大子段和即可。
注意,前面将哨兵节点的 val[] 设为了 0,在记录 mx[] 时不能记录它们的 val[]。
code
//在 pushup 函数中:
mx[x]=max(x>2?val[x]:-inf,max(mx[l],mx[r]));
//在 main 函数中:
mx[0]=-inf;//防止缺少儿子的节点 mx[] 更新出错
blabla..
if(op[2]=='X'){printf("%d\n",mx[rt]<0?mx[rt]:ms[rt]);continue;}
再次提交:
90pts MLE on #8
emm……
再仔细读题发现:
任何时刻数列中最多含有 5 * 10^5 个数,
插入的数字总数不超过 4 * 10^6。
我们可以通过回收节点来减小数组大小。
在删除一段区间时,遍历对应的所有节点,将它们的编号放入一个栈方便。
在新建节点时,若栈中有节点编号闲置则使用,否则 ++num。
因为这一编号可能是使用过的,所以需要清空标记等信息否则会获得 10pts AC on #1 的好成绩。
(事实上大部分信息会在 pushup 中更新,这里初始化懒标记即可)
code
void recycle(int u){
if(!u) return;
recycle(ch[u][0]);
stk[++tp]=u;
recycle(ch[u][1]);
}
int build(int l,int r,int f){
if(l>r) return 0;
int mid=(l+r)>>1,u=tp?stk[tp--]:++num;
ch[u][0]=build(l,mid-1,u);
val[u]=read(),fa[u]=f,chg[u]=inf,tag[u]=0;
ch[u][1]=build(mid+1,r,u);
pushup(u);
return u;
}
//在 main 函数中:
case 'D':recycle(work);work=0;break;
到这里即可 AC 本题。
最后应该没必要贴出全部代码了,放一下 main 函数吧。
code
#define work ch[ch[rt][1]][0]
int main(){
fa[2]=rt=1,ch[1][1]=num=2,chg[1]=chg[2]=inf,mx[0]=-inf;
m=read(),q=read();insert();
for(char op[10];q--;){
scanf(" %s",op);
if(op[2]=='X'){printf("%d\n",mx[rt]<0?mx[rt]:ms[rt]);continue;}
n=read()+1,m=read();
if(op[0]=='I'){
splay(kth(n)),splay(kth(n+1),rt);
insert();
continue;
}
splay(kth(n-1)),splay(kth(n+m),rt);
switch(op[0]){
case 'G':printf("%d\n",sum[work]);continue;
case 'D':recycle(work);work=0;break;
case 'R':turn(work);break;
case 'M':change(work,read());
}
pushup(ch[rt][1]),pushup(rt);
}
return 0;
}