[SCOI2010]序列操作 题解
前言
这道题很经典,特别是对于线段树的这个板块来说,完成后对线段树的理解会上一个档次。但是说实话,就算知道了这道题的思路,想要完成也很难,因为这个代码实在是太长了,而且线段树维护的变量态度多,很容易把人写晕,所以我就来发这一发题解,点明其中易错的几个点(尤其是我错的),以及每一个函数的详细解答,希望dalao们勿喷。
提示
如果想完成这道题,那么你至少要会以下操作:
- 线段树的区间修改,区间查询和(即P3372 【模板】线段树 1)
- 线段树的区间修改,区间最长连续的1(即P2894 [USACO08FEB]Hotel G)
言归正传
- 先说一个我做题的细节,因为原题中的下标是从0开始的,与我们(不包括所有人)的习惯不符合,所以我把每个下标人为地加一,即让线段树的区间从零开始。
- 紧接着我们来说线段树需要维护的变量:
struct Tree{
int l,r,len,sum0,sum1,lm0,rm0,lm1,rm1,la,num,tag;
}tree[400005];
这里数组开到了4*100000是因为要维护100000个节点,而线段树开数组开四倍维护的节点是比较适合的,不会超空间。
前方高能:这里来说一下维护的每个变量的意思:
- l:区间的左端点
- r:区间的右端点
- len:区间的长度,即r-l+1
- sum0:区间最长的连续0
- sum1:区间最长的连续1
- lm0:区间从左端点起的最长连续0
- lm1:区间从左端点起的最长连续1
- rm0:区间从右端点起的最长连续0
- rm1:区间从右端点起的最长连续1
- num:区间内1的个数
- la:区间赋值的懒标记(0表示无操作,1表示赋值为0,2表示赋值为1)
- tag:区间翻转的懒标记(0表示不翻转,1表示要翻转)
做过P2894 [USACO08FEB]Hotel G的小伙伴们都知道,那道题维护只维护了sum,lm,rm,却没有分0和1,这里分开维护主要是因为取反操作(后面会详细讲解)
首先是push_down操作,即怎样把懒标记下传
inline void push_down(int root){
if(tree[root].la!=0){
if(tree[root].la==1){
tree[root<<1].la=tree[root<<1|1].la=tree[root].la;
tree[root<<1].num=tree[root<<1].sum1=tree[root<<1].lm1=tree[root<<1].rm1=0;
tree[root<<1].lm0=tree[root<<1].rm0=tree[root<<1].sum0=tree[root<<1].len;
tree[root<<1|1].num=tree[root<<1|1].sum1=tree[root<<1|1].lm1=tree[root<<1|1].rm1=0;
tree[root<<1|1].lm0=tree[root<<1|1].rm0=tree[root<<1|1].sum0=tree[root<<1|1].len;
}
if(tree[root].la==2){
tree[root<<1].la=tree[root<<1|1].la=tree[root].la;
tree[root<<1].num=tree[root<<1].sum1=tree[root<<1].lm1=tree[root<<1].rm1=tree[root<<1].len;
tree[root<<1].lm0=tree[root<<1].rm0=tree[root<<1].sum0=0;
tree[root<<1|1].num=tree[root<<1|1].sum1=tree[root<<1|1].lm1=tree[root<<1|1].rm1=tree[root<<1|1].len;
tree[root<<1|1].lm0=tree[root<<1|1].rm0=tree[root<<1|1].sum0=0;
}
tree[root<<1].tag=tree[root<<1|1].tag=0;
}
if (tree[root].tag==1){
tree[root<<1].tag^=1;
tree[root<<1|1].tag^=1;
tree[root<<1].num=tree[root<<1].len-tree[root<<1].num;
tree[root<<1|1].num=tree[root<<1|1].len-tree[root<<1|1].num;
swap(tree[root<<1].sum1,tree[root<<1].sum0);
swap(tree[root<<1|1].sum1,tree[root<<1|1].sum0);
swap(tree[root<<1].lm0,tree[root<<1].lm1);
swap(tree[root<<1].rm0,tree[root<<1].rm1);
swap(tree[root<<1|1].lm0,tree[root<<1|1].lm1);
swap(tree[root<<1|1].rm0,tree[root<<1|1].rm1);
}
tree[root].la=0,tree[root].tag=0;
}
额。。。可能稍微有那么一点点长,毕竟有这么多的变量嘛。在这里先声明一个东西,就是这里懒标记中的赋值和翻转的优先顺序,是先赋值,再翻转(比如此时的la=1,tag=1,则这个区间为先覆盖为0,然后翻转,即全为1) 所以下传懒标记时,先讨论覆盖操作:
1.tree[root].la==0
: 显然不用进行任何操作
2.tree[root].la==1
: 即这个区间的每个值都要赋值成1,那么它的两个儿子区间的值都要赋值成1,所以两个儿子区间的懒标记都是1,然后再来看其它的变量:
- num: 既然都为0了,那么1的个数也为0.
- sum1:1都没有了,连续的1也是0个
- sum0: 现在最长的连续0就是区间长度
- lm0:整个区间都是0,从左起最长的连续0自然也是区间长度那么多
- lm1:没有1,lm1=0
- rm0,lm1同上
3.tree[root].la==2
:与tree[root].la==1
相反
思考(易错):为什么在下传la的时候,要加一行代码:tree[root<<1].tag=tree[root<<1|1].tag=0;
解答:因为不管前面进行了怎样的操作,覆盖以后就是覆盖的值,这是不可争辩的,所以前面的翻转也就不能作数了。
然后是懒标记tag,由于是翻转所,以0和1互换,所以维护的0和1的值也互换(是不是很好理解),对于num就变成区间长度-num,因为假设原来有num个1,那么就有len-num个0,互换以后就有len-num个1,说到这里,读者便可以理解为什么0和1要分开维护,这样才能在取反时进行维护。重点说一下这里的tag下传,因为这个区间翻转所以它的子区间也要翻转,即代码:
tree[root<<1].tag^=1;
tree[root<<1|1].tag^=1;
这里一定是把子区间的tag异或1而不是赋值成1,因为这个子区间可能原来就需要翻转一次,这次再翻转,就变成原来的样子了。最后清空当前节点的懒标记,那么push_down操作就完美地写成了。
push_up操作,即更改完成子区间后,向上回溯的过程
inline void push_up(int root){
if(tree[root<<1].sum0==tree[root<<1].len){
tree[root].lm0=tree[root<<1].len+tree[root<<