NOI2005 维护数列 题解 (洛谷 P2042) splay

前言

语文老师布置了随笔的作业,要求每周两篇,题材字数不限。
我对着我贫瘠的人生沉思了一会儿,决定用题解蒙混过关。

正文

link

区间翻转,一眼 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;
}
posted @ 2022-01-08 20:03  XG0000  阅读(426)  评论(1编辑  收藏  举报