0.线段树的介绍及其基本操作
线段树是一种动态维护和查询区间性质的一种高级数据结构,具体定义即简介可见蓝书P210-211。主要操作包括build(建树)、query(区间查询)、modify/update(点/区间修改)、pushup(上传值)、pushdown(下传懒标记)。下面以一道例题来介绍上述的几种操作:
例题0:AcWing 1275.最大数(JSOI2008)
这题还是比较简单,只要建树时开辟最长区间长为m的线段树,然后每次对于A操作,单点修改第n+1个数即可;对于Q操作,查询区间[n-x+1,n]即可。这些都是线段树的基本操作。下面介绍一下基本的函数实现:
线段树数组定义:
struct Node{
int l,r;//左右端点
int v;//本题中是最大值
}tr[4*N+5];//一般开4倍才不会越界
1.build
void build(int u,int l,int r){
tr[u].l=l,tr[u].r=r;//注意要先存左右端点再结束递归
if(l==r) return;
int mid=(l+r)>>1;
build(u<<1,l,mid);//递归左儿子
build(u<<1|1,mid+1,r);//递归右儿子
//pushup(u);这句在这里可以不用加,因为一开始没有数
}
2.pushup
void pushup(int u){
tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}
3.query
int query(int u,int l,int r){
//这里的l,r是查询范围,不是节点的范围
if(l<=tr[u].l && tr[u].r<=r) return tr[u].v;
//若整个节点的范围被查询范围包括,直接返回
int mid=(tr[u].l+tr[u].r)>>1;
int v=0;
if(l<=mid) v=query(u<<1,l,r);
//左儿子与查询范围有交集
if(r>mid) v=max(v,query(u<<1|1,l,r));
//右儿子与查询范围有交集
return v;
}
4.modify
void modify(int u,int x,int d){
//x是要修改的点,d是该节点要改成的值
if(tr[u].l==tr[u].r){
//搜到了要修改的单点,直接修改
tr[u].v=d;
return;
}
int mid=(tr[u].l+tr[u].r)>>1;
if(x<=mid) modify(u<<1,x,d);//x在左半边
else modify(u<<1|1,x,d);//x在右半边
pushup(u);//子区间修改后要更新父区间
}
有了这几种基本的函数,就可以轻松过掉这一个省选题了~
附加例题:AcWing 242.一个简单的整数问题
这题本来是树状数组里的一题,但也可以用线段树做一下练练手。这题就是基本的单点修改和区间查询,非常简单。
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
int a[N+5];
struct Node{
int l,r;
int d;
}tr[4*N+5];
void pushup(int u){
tr[u].d=tr[u<<1].d+tr[u<<1|1].d;
}
void build(int u,int l,int r){
if(l==r){
tr[u]={
l,r,a[l]-a[l-1]};
}
else{
tr[u]={
l,r};
int mid=(l+r)>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
}
int query(int u,int l,int r){
if(tr[u].l>=l && tr[u].r<=r) return tr[u].d;
int mid=(tr[u].l+tr[u].r)>>1;
int res=0;
if(l<=mid) res+=query(u<<1,l,r);
if(r>mid) res+=query(u<<1|1,l,r);
return res;
}
void modify(int u,int x,int d){
if(tr[u].l==tr[u].r){
tr[u].d+=d;
}
else{
int mid=(tr[u].l+tr[u].r)>>1;
if(x<=mid) modify(u<<1,x,d);
else modify(u<<1|1,x,d);
pushup(u);
}
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
build(1,1,n);
while(m--){
char op;
cin>>op;
if(op=='Q'){
int x;
scanf("%d",&x);
printf("%d\n",query(1,1,x));
}
else{
int l,r,d;
scanf("%d%d%d",&l,&r,&d);
modify(1,l,d);
if(r+1<=n) modify(1,r+1,-d);
}
}
return 0;
}
P.S.:其实所有的树状数组的题都可以用线段树实现。因为树状数组所支持的操作是单点修改和求前缀和,这恰好对应了线段树所支持的基本操作:单点修改和区间求和。
1.维护区间多个信息的线段树
例题1:AcWing 245.你能回答这些问题吗?
这题要维护的区间信息是最大连续子段和,显然需要用多个不同的信息来维护,那么我们考虑加入左端最大连续子段和、右端最大连续子段和。那么这个信息就可以高效地得到维护了,这种储存多个信息来维护并导出所求信息的方法很常见,在动态规划中也有涉及:如AcWing 283.多边形。代码如下:
#include<iostream>
#include<cstdio>
using namespace std;
const int N=5e5;
int a[N];
struct Node{
int l,r;
int sum,dat;//sum表示区间和,dat表示最大连续子段和
int lmax,rmax;//lmax表示区间包括左端点的最大连续子段和,rmax类似
}tr[N*4];
void pushup(Node &a,Node &b,Node &c){
//要利用图形来想情况
a.sum=b.sum+c.sum;
a.lmax=max(b.lmax,b.sum+c.lmax);
a.rmax=max(c.rmax,c.sum<