P3870 [TJOI2009] 开关 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意
给定一段区间,对该区间的灯作相反操作(开的变关,关的变开)。
思路
如果暴力模拟,那肯定会超时,不然题目给个线段树的标签干什么。
而线段树的优点正是可以快速地对一段区间进行同样操作,并且其时间复杂度降到了。
先复习一下线段树吧,虽然我也刚学不久:线段树的用处就是对编号连续的一些点进行修改或者统计操作,本质上是维护下标为1,2,..,n的n个按顺序排列的数的信息,即是维护n的个点的信息,至于每个点的数据的含义可以有很多。
把一段区间不断分成
和
两段区间,其中
(整数除法,而且有点分治那味了),而且对任意的
的分解都是唯一的,我的理解就是改变了到达某一个下标的方式。
在做操作的同时,还引入了一个叫做“懒惰标记”的东西,它起到了延迟操作,防止操作堆积的作用。
简要讲讲就够,讲的也不太好。
因为灯要开关,因此想到物理中的高低电平,可以用和
分别表示灯的开和关的状态。
但是考虑到:按照正常人的思维,一般会通过判断来实现和
之间的转化,未免太麻烦了。于是可以用一种奇妙的方法:异或(^)
当状态为时,对它^=1,就有
当状态为时,对它^=1,就有
这样就易如反掌的实现了开关灯的操作(其实1和-1转换应该也可以),而且也方便了更新状态,减少了大幅的码量!
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll z=500005;
ll n,m,mod,a[z],m1[z],m2[z],tree[z],zt,x,y,ans;
//线段树搓板子,不过将+改为了^
void pushdown(ll p,ll l,ll r)//更新,此处为异或1,因此对懒惰节点的更新方式为^=1
{
if(m2[p]==0)return;
m2[p*2]^=1,m2[p*2+1]^=1;//lazytag
ll mid=(l+r)/2;
tree[p*2]=(mid-l+1)-tree[p*2];
tree[p*2+1]=(r-mid)-tree[p*2+1];
m2[p]=0;
}
void addpoint2(ll l,ll r,ll p,ll nowl,ll nowr)//作异或
{
if(l<=nowl&&r>=nowr)//区间包含
{
tree[p]=(nowr-nowl+1)-tree[p];
m2[p]^=1;
return;
}
pushdown(p,nowl,nowr);//否则更新节点,准备继续分解
ll mid=(nowl+nowr)/2;
if(l<=mid)addpoint2(l,r,p*2,nowl,mid);//左子树
if(r>mid)addpoint2(l,r,p*2+1,mid+1,nowr);//右子树
tree[p]=tree[p*2]+tree[p*2+1];//父节点收集左右两个子节点的信息
}
ll chaxun(ll l,ll r,ll p,ll nowl,ll nowr)//查询,原理与上面大致相同
{
if(l<=nowl&&r>=nowr)return tree[p];
pushdown(p,nowl,nowr);
ll mid=(nowl+nowr)/2,a=0,b=0;
if(l<=mid)a=chaxun(l,r,p*2,nowl,mid);
if(mid<r)b=chaxun(l,r,p*2+1,mid+1,nowr);
return a+b;
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%lld",&zt);
if(zt==0)
{
scanf("%lld%lld",&x,&y);
addpoint2(x,y,1,1,n);
}
if(zt==1)
{
scanf("%lld%lld",&x,&y);
printf("%lld\n",chaxun(x,y,1,1,n));
}
}
return 0;
}