线段树复习
线段树复习
还有将近一个月就要noip了,一定要加油啊!
线段树概念
线段树是为了解决一些区间(单点)修改和区间(单点)查询的数据结构。它将每个区间[l,r]划分为[l,mid]和[mid+1,r],其中mid=(l+r)/2。这样我们就可以通过两个小区间的信息快速得到大区间的信息。
建树
假如用u来表示当前节点的编号,则用\(u \times 2\)表示左儿子,用\(u \times 2 + 1\)表示右儿子。这棵树的叶子节点表示每个单点。
修改操作
对于单点的修改操作,我们只用修改整棵树的一条链即好。对于区间修改,如果此区间在一个节点上,则继续递归下去,否则我们就将区间依照建树的方法分割为两个区间继续递归。当我们发现有一个节点的区间恰好被修改区间覆盖时,我们可以打一个lazy标记,表示这段区间已经被修改,而不是继续递归。当我们要查询它的儿子时则将标记下放。总体时间复杂度\(O(n \log n)\)
查询操作
查询操作其实与修改操作大同小异,不过要记得标记下放。复杂度同样为\(O(n \log n)\)
模板(区间求和和区间修改)
#include<cstdio>
#include<cstring>
#include<iostream>
#define R(a) (a << 1)
#define L(a) (a << 1|1)
using namespace std;
#define maxn 200002
#define LL long long
struct node{
int l,r;
LL sum,add;
}N[maxn << 2];
LL A[maxn];
void PushUp(const int& u){
N[u].sum = N[R(u)].sum + N[L(u)].sum;
}
void Build(const int& u,const int& l,const int& r){
N[u].l = l,N[u].r = r;
if(l == r){
N[u].sum = A[l];
return;
}
int mid = (l + r)>>1;
Build(L(u),l,mid);
Build(R(u),mid + 1,r);
PushUp(u);
}
void Pushdown(const int& u)
{
N[R(u)].add += N[u].add;
N[R(u)].sum += (N[R(u)].r - N[R(u)].l + 1)*N[u].add;
N[L(u)].add += N[u].add;
N[L(u)].sum += (N[L(u)].r - N[L(u)].l + 1)*N[u].add;
N[u].add = 0;
}
LL Query(const int& u,const int& l,const int& r){
if(l <= N[u].l && r >= N[u].r) return N[u].sum;
if(N[u].add) Pushdown(u);
int mid = (N[u].l + N[u].r) >> 1;
if(r <= mid) return Query(L(u),l,r);
if(l > mid) return Query(R(u),l,r);
return Query(L(u),l,mid) + Query(R(u),mid + 1,r);
}
void Updata(const int& u,const int& l,const int& r,const LL& c){
if(l <= N[u].l && r >= N[u].r) {
N[u].add += c;
N[u].sum +=(N[u].r - N[u].l + 1) * c;
return;
}
N[u].sum +=(r - l + 1)*c;
if(N[u].add) Pushdown(u);
int mid = (N[u].l + N[u].r) >> 1;
if(r <= mid) Updata(L(u),l,r,c);
else if(l > mid) Updata(R(u),l,r,c);
else {
Updata(L(u),l,mid,c);
Updata(R(u),mid+1,r,c);
}
}
int main()
{
int n,m;
scanf("%d",&n);
for(int i = 1;i <= n;++i)scanf("%lld",&A[i]);
scanf("%d",&m);
Build(1,1,n);
int a,b;
LL c;
int Q;
while(m--)
{
scanf("%d",&Q);
if(Q == 2){
scanf("%d%d",&a,&b);
printf("%lld\n",Query(1,a,b));
}else {
scanf("%d%d%lld",&a,&b,&c);
Updata(1,a,b,c);
}
}
return 0;
}
一些例题
一些有简单的线段树题目bzoj1018,bzoj1858,bzoj1493。
这些题目都是通过线段树维护一些特殊信息来的出答案。
bzoj1018: [SHOI2008]堵塞的交通traffic
此题我们用线段树维护每段区间的四个顶点的联通情况(不通过外部节点)。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int Maxn = 100005;
#define ls (u<<1)
#define rs (u<<1|1)
#define MID(a,b) int mid = (a+b) >> 1;
struct node {
bool a[2], b[2], c[2];
node() {}
}N[Maxn << 2];
int C, r1, r2, c1, c2;
bool edge[Maxn][3];
void build(int u,int l,int r) {
if (l == r) {
N[u].b[0] = N[u].b[1] = true; return;
}
MID(l,r);
build(ls,l,mid); build(rs,mid+1,r);
}
node pluse(node a, node b,bool x, bool y) {
node c;
c.a[0] = (a.a[0])||(a.b[0]&&a.b[1]&&x&&y&&b.a[0]);
c.a[1] = (b.a[1])||(b.b[0]&&b.b[1]&&x&&y&&a.a[1]);
c.b[0] = (a.b[0]&&x&&b.b[0])||(a.c[0]&&y&&b.c[1]);
c.b[1] = (a.b[1]&&y&&b.b[1])||(a.c[1]&&x&&b.c[0]);
c.c[0] = (x&&a.b[0]&&b.c[0])||(y&&a.c[0]&&b.b[1]);
c.c[1] = (y&&a.b[1]&&b.c[1])||(x&&a.c[1]&&b.b[0]);
return c;
}
void update(int u,int l,int r,int val) {
if (l == r) {
N[u].b[0] = N[u].b[1] = true;
N[u].a[0] = N[u].a[1] = N[u].c[0] = N[u].c[1] = edge[val][2];
return;
}
MID(l,r);
if (val <= mid) update(ls,l,mid,val);
else update(rs,mid+1,r,val);
N[u] = pluse(N[ls],N[rs],edge[mid][0],edge[mid][1]);
}
node query(int u,int l,int r,int x, int y) {
if (l >= x && r <= y) return N[u];
MID(l,r);
if (y <= mid) return query(ls,l,mid,x,y);
else if (x > mid) return query(rs,mid+1,r,x,y);
return pluse(query(ls,l,mid,x,mid),query(rs,mid+1,r,mid+1,y),edge[mid][0],edge[mid][1]);
}
int main() {
scanf("%d", &C);
build(1,1,C);
char S[10] = {0};
node tmp1, tmp2, tmp3; bool ans;
while (scanf("%s",S)) {
if (*S == 'E') break;
scanf("%d%d%d%d",&r1,&c1,&r2,&c2);
--r1, --r2;
if (c1 > c2) swap(r1,r2), swap(c1,c2);
if (*S == 'O') {
if (r1 > r2) swap(r1,r2), swap(c1,c2);
if (r1 < r2) {
edge[c1][2] = 1; update(1,1,C,c1);
} else {
edge[c1][r1] = 1; update(1,1,C,c1);
}
} else if (*S == 'C') {
if (r1 > r2) swap(r1,r2), swap(c1,c2);
if (r1 < r2) {
edge[c1][2] = 0; update(1,1,C,c1);
} else {
edge[c1][r1] = 0; update(1,1,C,c1);
}
} else {
tmp1 = query(1,1,C,1,c1), tmp2 = query(1,1,C,c1,c2), tmp3 = query(1,1,C,c2,C);
if (r1 && r2) {
ans=tmp2.b[1]||(tmp1.a[1]&&tmp2.c[0])||(tmp2.c[1]&&tmp3.a[0]);
ans = ans || (tmp1.a[1]&&tmp2.b[0]&&tmp3.a[0]);
} else if (! r1 ^ r2) {
ans=tmp2.b[0]||(tmp1.a[1]&&tmp2.c[1])||(tmp2.c[0]&&tmp3.a[0]);
ans = ans || (tmp1.a[1]&&tmp2.b[1]&&tmp3.a[0]);
} else if (r1) {
ans=tmp2.c[1]||(tmp1.a[1]&&tmp2.b[0])||(tmp2.b[1]&&tmp3.a[0]);
ans = ans || (tmp1.a[1]&&tmp2.c[0]&&tmp3.a[0]);
} else {
ans=tmp2.c[0]||(tmp1.a[1]&&tmp2.b[1])||(tmp2.b[0]&&tmp3.a[0]);
ans = ans || (tmp1.a[1]&&tmp2.c[1]&&tmp3.a[0]);
}
if (ans) puts("Y"); else puts("N");
}
}
return 0;
}
// a x b
// c y d
//a[] means a-c b-d
//b[] means a-b c-d
//c[] means a-d b-c
bzoj1858: [Scoi2010]序列操作
此题中每个节点维护区间中的连续段数和左右端点颜色即可。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int ina; char inc;
inline int geti() {
while((inc=getchar())<'0'||inc>'9'); ina=inc-'0';
while((inc=getchar())>='0'&&inc<='9')ina=(ina<<3)+(ina<<1)+inc-'0';
return ina;
}
#define N 100005
#define cmax(a,b) ((a)<(b)?(a)=(b):1)
struct D {
int l,r,l0,r0,l1,r1,mx0,mx1,sum0,sum1,re,c,all;
}C[N<<2];
#define ls u<<1
#define rs u<<1|1
inline void rever(int u) {
swap(C[u].l0,C[u].l1); swap(C[u].r0,C[u].r1);
swap(C[u].mx0,C[u].mx1); swap(C[u].sum0,C[u].sum1);
if(~C[u].all) C[u].all^=1;
}
inline void modify(int u,int co) {
C[u].all=co; C[u].re=0;
if(co) {
C[u].sum1=C[u].l1=C[u].r1=C[u].mx1=C[u].r-C[u].l+1;
C[u].sum0=C[u].l0=C[u].r0=C[u].mx0=0;
} else {
C[u].sum0=C[u].l0=C[u].r0=C[u].mx0=C[u].r-C[u].l+1;
C[u].sum1=C[u].l1=C[u].r1=C[u].mx1=0;
}
}
D operator + (const D&a,const D&b) {
D t; t.l=a.l,t.r=b.r;t.re=0; t.c=-1;
t.l0=a.l0,t.l1=a.l1;t.r0=b.r0,t.r1=b.r1;
t.mx0=max(a.mx0,b.mx0);t.mx1=max(a.mx1,b.mx1);
cmax(t.mx0,a.r0+b.l0),cmax(t.mx1,a.r1+b.l1);
t.sum0=a.sum0+b.sum0,t.sum1=a.sum1+b.sum1;
if(a.all==0) t.l0=a.mx0+b.l0; else if(a.all==1) t.l1=a.mx1+b.l1;
if(b.all==0) t.r0=b.mx0+a.r0; else if(b.all==1) t.r1=b.mx1+a.r1;
(a.all^b.all)?t.all=-1:t.all=a.all; return t;
}
inline void Down(int u) {
if(C[u].l==C[u].r) return;
if(~C[u].c) {
modify(ls,C[u].c),modify(rs,C[u].c);
C[ls].c=C[rs].c=C[u].c; C[u].c=-1;
}
if(C[u].re){
C[ls].re^=1,C[rs].re^=1;
rever(ls), rever(rs);
C[u].re=0;
}
}
void Build(int u,int l,int r) {
C[u].l=l,C[u].r=r,C[u].c=-1;
if(l==r) {modify(u,geti());return;}
int mid=l+r>>1;
Build(ls,l,mid);Build(rs,mid+1,r); C[u]=C[ls]+C[rs];
}
void Update(int u,int x,int y,int op) {
int l=C[u].l,r=C[u].r; Down(u);
if(x==l&&r==y) {
if(op<2) modify(u,op),C[u].c=op;
else rever(u),C[u].re=1;
return;
}int mid=(l+r)>>1;
if(y<=mid) Update(ls,x,y,op);
else if(x>mid) Update(rs,x,y,op);
else Update(ls,x,mid,op),Update(rs,mid+1,y,op);
C[u]=C[ls]+C[rs];
}
D QC(int u,int x,int y) {
int l=C[u].l,r=C[u].r; Down(u);
if(x==l&&r==y) return C[u];
int mid=(l+r)>>1;
if(y<=mid) return QC(ls,x,y);
else if(x>mid) return QC(rs,x,y);
return QC(ls,x,mid)+QC(rs,mid+1,y);
}
int QSum(int u,int x,int y) {
int l=C[u].l,r=C[u].r; Down(u);
if(l==x&&r==y) return C[u].sum1;
int mid=(l+r)>>1;
if(y<=mid) return QSum(ls,x,y);
else if(x>mid) return QSum(rs,x,y);
else return QSum(ls,x,mid) + QSum(rs,mid+1,y);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("operator.in","r",stdin);
freopen("operator.out","w",stdout);
#endif
int n=geti(),m=geti(),x,y,op;
Build(1,0,n-1);
while(m--) {
op=geti(),x=geti(),y=geti();
if(op<3) Update(1,x,y,op);
else if(op==3) printf("%d\n",QSum(1,x,y));
else printf("%d\n",QC(1,x,y).mx1);
}return 0;
}
bzoj1493: [NOI2007]项链工厂
此题同上一道题有些相似,只不过多了个旋转和翻转操作比较麻烦。
其实一般来说,线段树是无法做区间翻转的,不过此题是整个区间翻转,于是就可以用线段树乱搞了。
#include <cstdio>
#include <cstring>
int ina; char inc;
inline int geti() {
for(;(inc=getchar())<'0'||inc>'9';);
for(ina=inc-'0';(inc=getchar())>='0'&&inc<='9';ina=(ina<<3)+(ina<<1)+inc-'0');
return ina;
}
#define ls u<<1
#define rs u<<1|1
#define N 500010
struct D{
int lc,rc,pa;
D operator + (const D&a) const {
D ret; ret.lc=lc,ret.rc=a.rc;
ret.pa=pa+a.pa; if(rc==a.lc) --ret.pa;
return ret;
}
inline void ch(int a) {lc=rc=a; pa=1;}
}C[N<<2];
int ly[N<<2],n,c,dis,q; bool d;
inline void Down(int u){
if(!ly[u]) return;
ly[ls]=ly[rs]=ly[u];
C[ls].ch(ly[u]),C[rs].ch(ly[u]);
ly[u]=0;
}
void Update(int u,int l,int r,int x,int y,int op) {
if(x<=l&&r<=y) {
ly[u]=op; C[u].ch(op);
return;
}Down(u);
int m=l+r>>1;
if(y<=m) Update(ls,l,m,x,y,op);
else if(x>m) Update(rs,m+1,r,x,y,op);
else {Update(ls,l,m,x,m,op);Update(rs,m+1,r,m+1,y,op);}
C[u]=C[ls]+C[rs];
}
inline void cal(int &x,int &y) {
if(d){
x=(n+2-x-dis+n)%n;
y=(n+2-y-dis+n)%n;
x^=y^=x^=y;
}else x=(x-dis+n)%n,y=(y-dis+n)%n;
(!x)?x=n:1,(!y)?y=n:1;
}
int find(int u,int l,int r,int p) {
if(l==r) return C[u].lc;
Down(u); int m=l+r>>1;
return (p<=m)?find(ls,l,m,p):find(rs,m+1,r,p);
}
D Query(int u,int l,int r,int x,int y){
if(x<=l&&r<=y) return C[u];
Down(u); int m=l+r>>1;
if(y<=m) return Query(ls,l,m,x,y);
if(x>m) return Query(rs,m+1,r,x,y);
return Query(ls,l,m,x,m)+Query(rs,m+1,r,m+1,y);
}
void Build(int u,int l,int r) {
if(l==r) {C[u].ch(geti()); return;}
int m=l+r>>1; Build(ls,l,m); Build(rs,m+1,r);
C[u]=C[ls]+C[rs];
}
int main() {
#ifndef ONLINE_JUDGE
freopen("T.in","r",stdin);
#endif
char ts[3]; int x,y,z,cx,cy;
n=geti(),c=geti(); Build(1,1,n);
for(q=geti();q;--q) {
scanf("%s",ts);
if(*ts=='R') {
z=geti();
if(d)dis=(dis+n-z)%n;
else dis=(dis+z)%n;
}
else if(*ts=='F') d=!d;
else if(*ts=='P') {
x=geti(),y=geti(),z=geti(); cal(x,y);
if(x>y) Update(1,1,n,x,n,z),Update(1,1,n,1,y,z);
else Update(1,1,n,x,y,z);
} else if(*ts=='S') {
x=geti(),y=geti(); cal(x,y);
cx=find(1,1,n,x),cy=find(1,1,n,y);
Update(1,1,n,x,x,cy); Update(1,1,n,y,y,cx);
} else if(ts[1]=='S') {
x=geti(),y=geti(); cal(x,y);
if(x>y) printf("%d\n",(Query(1,1,n,x,n)+Query(1,1,n,1,y)).pa);
else printf("%d\n",Query(1,1,n,x,y).pa);
} else {
z=Query(1,1,n,1,n).pa-(C[1].lc==C[1].rc); (z<1)?z=1:0;
printf("%d\n",z);
}
}return 0;
}
总结
总的来说,线段树的适用范围是对于一系列区间修改和查询,一般不支持翻转操作。而且我们可以通过子区间快速得到大区间的答案(一般为\(O(1)\)),亦可以打标记和快速下放标记(或者采用神奇的永久化标记)。其建树复杂度一般为\(O(n)\),查询和修改一般为\(O(\log n)\)。(当区间合并和下放标记复杂度不是\(O(1)\)时,需再乘上其他多项式)。
Ps:线段树的题目一定要思路清晰,不可以有思路就开打,因为它很难debug,一般只能采用静态查错。