[SCOI2010]序列操作

细节巨大多的线段树,调了四个小时,恶心心。

Description

给定一个 01 序列,要求完成五种操作:

  • 0 l r\([l,r]\) 全部赋值为 \(0\)
  • 1 l r\([l,r]\) 全部赋值为 \(1\)
  • 2 l r\([l,r]\) 取反;
  • 3 l r 查询 \([l,r]\)\(1\) 的个数;
  • 4 l r 查询 \([l,r]\) 最多有多少个连续的 \(1\)

Solution

Part 1

说一下我维护的变量:

#define ls x<<1
#define rs x<<1|1
int lazy[maxn<<2][4];
struct tree{
  int len;
  int len0,sum0,ml0,mr0;
  int len1,sum1,ml1,mr1;
}tr[maxn<<2];
//len0/1 : 0/1 的最长连续个数
//sum0/1 : 0/1 的个数
//ml0/1 : 从左端点开始的 0/1 的最长连续个数
//mr0/1 : 从右端点开始的 0/1 的最长连续个数
//lazy[x][1] = 1/2 : 第 0/1 种操作的区间懒标记
//lazy[x][3] = 1 : 第 2 种操作的区间懒标记

Part 2

除翻转操作外,其余的可以用维护区间最大子段和的思想来处理。

初始建树时,对于序列中的每一位,根据其是 \(0\) 还是 \(1\) 来进行相应信息的处理。

void build(int x,int l,int r){
  tr[x].len=r-l+1;
  if(l==r){
    if(a[l]) tr[x].len1=tr[x].sum1=tr[x].ml1=tr[x].mr1=1;
    else tr[x].len0=tr[x].sum0=tr[x].ml0=tr[x].mr0=1;
    return;
  }
  int mid=l+r>>1;
  build(ls,l,mid);
  build(rs,mid+1,r);
  pushup(x);
}

Part 3

信息向上合并时,我们采用如下处理方式:

  1. 区间内相应颜色的总个数直接相加即可。

  2. 区间最长连续段长度,等于左右儿子的最长连续段长度和中间的最长连续段长度的最大值
    因为可能区间内的最长连续段横跨左右儿子,此时单取左右儿子中的最大值难以维护。
    而横跨左右儿子的最长连续段,也就是同时包含左儿子右端点和右儿子左端点的连续段。

  3. 区间中包含左端点的最长连续段等于左儿子中包含左端点的最长连续段。
    特别的,当左儿子颜色 = 右儿子左端最长连续段颜色时,区间左端点最长连续段 = 左儿子长度 + 右儿子左端点最长连续段。
    也就是,当这个连续段横跨左右儿子且包含最左端时要更新。
    区间右端最长连续段同理。

void pushup(int x){
  if(tr[ls].sum0) tr[x].ml1=tr[ls].ml1;
  else tr[x].ml1=tr[ls].sum1+tr[rs].ml1;
    
  if(tr[rs].sum0) tr[x].mr1=tr[rs].mr1;
  else tr[x].mr1=tr[rs].sum1+tr[ls].mr1;
    
  if(tr[ls].sum1) tr[x].ml0=tr[ls].ml0;
  else tr[x].ml0=tr[ls].sum0+tr[rs].ml0;
    
  if(tr[rs].sum1) tr[x].mr0=tr[rs].mr0;
  else tr[x].mr0=tr[rs].sum0+tr[ls].mr0;
   
  tr[x].sum0=tr[ls].sum0+tr[rs].sum0;
  tr[x].sum1=tr[ls].sum1+tr[rs].sum1;
  tr[x].len0=max(max(tr[ls].len0,tr[rs].len0),tr[ls].mr0+tr[rs].ml0);
  tr[x].len1=max(max(tr[ls].len1,tr[rs].len1),tr[ls].mr1+tr[rs].ml1);
}

Part 4

知道了上面的处理方式后,三种修改操作就比较简单了。

  • 区间染色:将对应的颜色信息全部赋值为区间长度,其余的颜色信息全部清零;
  • 区间翻转:交换对应的信息即可。

应该比较好理解。

修改时,注意一种操作对其余的操作会产生什么影响。

我们发现,后进行的区间染色会覆盖先进行的区间翻转,而后进行的区间翻转等于将区间染色取反。

因此我们在更新和下传懒标记时要注意这一点。

所以对于懒标记的处理如下。

  • 区间染色:我们每次将对应懒标记更新并清空翻转标记,然后染色完成时再清空染色标记即可;
  • 区间翻转:由于两次翻转等于没有,所以我们每次将对应懒标记取反,只有懒标记为 \(\text{true}\) 时再进行翻转。
    有染色标记时直接取反染色标记即可。
void pushdown(int x){
  if(lazy[x][1]){
    lazy[x][3]=0;
    lazy[ls][3]=lazy[rs][3]=0;
    if(lazy[x][1]==1){
      lazy[ls][1]=lazy[rs][1]=1;
      tr[ls].ml1=tr[rs].ml1=0;
      tr[ls].mr1=tr[rs].mr1=0;
      tr[ls].len1=tr[rs].len1=0;
      tr[ls].sum1=tr[rs].sum1=0;
      tr[ls].ml0=tr[ls].mr0=tr[ls].len;
      tr[ls].len0=tr[ls].sum0=tr[ls].len;
      tr[rs].ml0=tr[rs].mr0=tr[rs].len;
      tr[rs].len0=tr[rs].sum0=tr[rs].len;
    }
    else{
      lazy[ls][1]=lazy[rs][1]=2;
      tr[ls].ml0=tr[rs].ml0=0;
      tr[ls].mr0=tr[rs].mr0=0;
      tr[ls].len0=tr[rs].len0=0;
      tr[ls].sum0=tr[rs].sum0=0;
      tr[ls].ml1=tr[ls].mr1=tr[ls].len;
      tr[rs].ml1=tr[rs].mr1=tr[rs].len;
      tr[ls].len1=tr[ls].sum1=tr[ls].len;
      tr[rs].len1=tr[rs].sum1=tr[rs].len;
    }
    lazy[x][1]=0;
  }

  if(lazy[x][3]){
    if(lazy[ls][1]!=0) lazy[ls][1]=3-lazy[ls][1];
    else lazy[ls][3]^=1;
    if(lazy[rs][1]!=0) lazy[rs][1]=3-lazy[rs][1];
    else lazy[rs][3]^=1;
    swap(tr[ls].ml0,tr[ls].ml1);
    swap(tr[rs].ml0,tr[rs].ml1);
    swap(tr[ls].mr0,tr[ls].mr1);
    swap(tr[rs].mr0,tr[rs].mr1);
    swap(tr[ls].len0,tr[ls].len1);
    swap(tr[rs].len0,tr[rs].len1);
    swap(tr[ls].sum0,tr[ls].sum1);
    swap(tr[rs].sum0,tr[rs].sum1);
    lazy[x][3]=0;
  }
}

另外,也因为我们后进行的操作会对先进行的产生影响,所以我们每次要把 pushdown 放在函数的最前面,保证在进行更新和查询时所有操作已被完成。

void update(int x,int l,int r,int L,int R,int tag){
  pushdown(x);
  if(L<=l&&R>=r){
    if(tag==1){
      lazy[x][1]=1;lazy[x][3]=0;
      tr[x].ml0=tr[x].mr0=tr[x].sum0=tr[x].len0=tr[x].len;
      tr[x].ml1=tr[x].mr1=tr[x].sum1=tr[x].len1=0;
    }
    else if(tag==2){
      lazy[x][1]=2;lazy[x][3]=0;
      tr[x].ml1=tr[x].mr1=tr[x].sum1=tr[x].len1=tr[x].len;
      tr[x].ml0=tr[x].mr0=tr[x].sum0=tr[x].len0=0;
    }
    else{
      lazy[x][3]^=1;
      swap(tr[x].ml0,tr[x].ml1);
      swap(tr[x].mr0,tr[x].mr1);
      swap(tr[x].len0,tr[x].len1);
      swap(tr[x].sum0,tr[x].sum1);
    }
    return;
  }
  int mid=l+r>>1;
  if(L<=mid) update(ls,l,mid,L,R,tag);
  if(R>=mid+1) update(rs,mid+1,r,L,R,tag);
  pushup(x);
}

Part 5

区间求 \(1\) 的个数,线段树常规操作,不赘述。

int query1(int x,int l,int r,int L,int R){
  pushdown(x);
  if(L<=l&&R>=r) return tr[x].sum1;
  int mid=l+r>>1,ans=0;
  if(L<=mid) ans+=query1(ls,l,mid,L,R);
  if(R>=mid+1) ans+=query1(rs,mid+1,r,L,R);
  return ans;
}

Part 6

区间求最长连续 \(1\) 的个数。

由于当这个连续段横跨左右儿子时,返回的答案需要用到维护的多个信息,所以我们选择返回一个结构体。

每次询问时,我们按上面处理的方式来找出最长连续段长度然后返回即可。

注意统计答案时多种信息都要合并。

  tree query2(int x,int l,int r,int L,int R){
  pushdown(x);
  if(L<=l&&R>=r) return tr[x];
  int mid=l+r>>1;
  if(L>mid) return query2(rs,mid+1,r,L,R);
  if(R<=mid) return query2(ls,l,mid,L,R);
  tree ans,sl,sr;
  sl=query2(ls,l,mid,L,R);
  sr=query2(rs,mid+1,r,L,R);
  ans.sum1=sl.sum1+sr.sum1;
    
  if(sl.sum1==sl.len) ans.ml1=sl.len+sr.ml1;
  else ans.ml1=sl.ml1;
  if(sr.sum1==sr.len) ans.mr1=sr.len+sl.mr1;
  else ans.mr1=sr.mr1;
  ans.len1=max(max(sl.len1,sr.len1),sl.mr1+sr.ml1);
  return ans;
}

Part 7

然后就处理完啦!

最后给出主函数:

int main(){
  n=read();m=read();
  for(int i=1;i<=n;i++)a[i]=read();
  Seg::build(1,1,n);
  for(int i=1,opt,x,y;i<=m;i++){
    opt=read();x=read()+1;y=read()+1;
    if(opt==0||opt==1||opt==2) Seg::update(1,1,n,x,y,opt+1);
    else if(opt==3) printf("%d\n",Seg::query1(1,1,n,x,y));
    else printf("%d\n",Seg::query2(1,1,n,x,y).len1);
  }
  return 0;
}

祝大家调 bug 愉快!