线段树**(Segment Tree)是一种基于分治思想的二叉树结构**,常用来维护 区间信息 的数据结构,可以在 O (log N)的时间复杂度内实现==单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)==等操作
节点编号:
根结点编号为1;
编号为 x 的节点的左孩子节点编号为 2x ,右孩子节点编号为 2x+1 .
因此,可以使用一个struct 数组保存线段树。通常,N个节点的满二叉树 有
N+N/2+N/4+…+2+1=2N - 1个节点,但是线段树最后一层会有空余,所以,存储线段树的数组规格 >= 4N,以保证不越界
给定一个长度为 N 的序列 A,便可以在区间 [ 1, N]上建一棵线段树,每个叶节点 [ i,i ] 存储 A[ i ]的值。线段树的二叉树结构可以很方便地从下向上传递信息。
递归建树:
根节点管辖的区间长度已经是1,则可以直接根据a数组上相应位置的值初始化该节点。
否则将该区间从中点处分割为两个子区间,分别进入左右子节点递归建树,最后合并两个子节点的信息。
/**/
void build(int s, int t, int p) {// 对[s,t]区间建立线段树,当前区间节点编号为 p
if (s == t) {
d[p] = a[s];
return;
}
int m = s + ((t - s) >> 1);
// 移位运算符的优先级小于加减法,所以加上括号
// 如果写成 (s + t) >> 1 可能会超出 int 范围
build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
// 递归对左右区间建树
d[p] = d[p * 2] + d[(p * 2) + 1];
}
/*以区间最大值问题为例*/
struct SegmentTree{
int l,r;
int dat;//区间最大值
}d[size*4];
//单点修改
void change(int x,int v,int p){//把 a[ x ]的值修改为 v
if(d[p].l==d[p].r){//找到叶节点
d[p].dat=v;
return;
}
int mid=(d[p].l+d[p].r)/2;
if(x<=mid) change(x,v,p*2);//x 属于左半区间
else change(x,v,p*2+1);//x 属于右半区间
d[p].dat=max(d[p*2].dat,d[p*2+1].dat);//从下往上更新信息
}
注意,在线段树中,根节点(编号为1的节点)是执行各个指令的入口,因此,需要从根节点出发进行操作。即
建树的调用入口:build (1, n, 1);
单点修改的调用入口:change(x, v, 1);
/*以区间最大值问题为例*/
struct SegmentTree{
int l,r;
int dat;//区间最大值
}d[size*4];
//区间查询
int ask(int l,int r,int p) {//查询序列 a 在区间 [l,r] 上的最大值
if(l<=d[p].l&&d[p].r<=r){//完全包含
return d[p].dat;
}
int mid(d[p].l+d[p].r)/2;
int val=-(1<<30);//负无穷大
if(l<=mid) val=max(val,ask(l,r,p*2));//左子节点有重叠
if(r>mid) val=max(val,ask(l,r,p*2+1));//右子节点有重叠
return val;
}
//调用入口 ask(l,r,1)
执行区间查询时,从根节点开始,递归查询
有以下三种情况:
1.要查询的 区间 [l,r] 完全包含当前编号p代表的区间 ,直接返回
2.l<=mid 左子节点有重叠,递归访问左子节点
3.r>mid 右子节点有重叠,递归访问右子节点
注意,l,r都位于 mid 的一侧时,只会递归一个子树;
当 == l,r位于mid两侧时 ,才会同时递归两棵子树,且这种情况只会出现一次 ==
在线段树区间查询中,当查询的 区间 [l,r] 完全包含当前编号p代表的区间时,直接返回存储的答案。在“区间修改”中,若某个节点被修改区间 [l,r] 完全覆盖,那么以该节点为根的整个子树的所有节点存储的信息都会发生变化,逐一修改,时间复杂度将会变为 O(N),并且如果之后的查询用不到此次修改的值,那么更新该节点的子树是毫无意义的。这时,选择在执行修改指令时,如果某个节点p代表的区间被修改区间 [l,r] 完全覆盖,就在节点p上添加一个懒标记,表示“该节点已经被修改,但其子节点还没有被更新”,这样仍能保证O (log N)的时间复杂度
《算法进阶指南》例题: 245. 你能回答这些问题吗
下面代码思路基本有了,但不能输入 QWQ
#include
using namespace std;
const int N=500005;
int n,m,a[N];
struct node{
//sum区间和,dat区间最大连续字段和
//lmax,rmax仅靠左,右端的最大连续字段和
int lmax,rmax,sum,dat,l,r;
}t[N*4];
void build(int l,int r,int p){
if(t[p].l==l&&t[p].r==r){
t[p].lmax=t[p].rmax=t[p].sum=t[p].dat=a[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,p*2);
build(mid+1,r,p*2+1);
t[p].sum=t[p*2].sum+t[p*2+1].sum;
t[p].lmax=max(t[p*2].lmax,t[p*2].sum+t[p*2+1].lmax);
t[p].rmax=max(t[p*2+1].rmax,t[p*2+1].sum+t[p*2].rmax);
t[p].dat=max(max(t[p*2].dat,t[p*2+1].dat),t[p*2].rmax+t[p*2+1].lmax);
}
//单点修改
/*void change(int x,int v,int p){
if(t[p].l==x&&t[p].r==x){
t[p].lmax=v;t[p].rmax=v;
t[p].sum=v;t[p].dat=v;
return;
}
int mid=t[p].l+(t[p].r-t[p].l)>>1;
if(x<=mid) change(x,v,p*2);
else change(x,v,p*2+1);
t[p].sum=t[p*2].sum+t[p*2+1].sum;
t[p].lmax=max(t[p*2].lmax,t[p*2].sum+t[p*2+1].lmax);
t[p].rmax=max(t[p*2+1].rmax,t[p*2+1].sum+t[p*2].rmax);
t[p].dat=max(t[p*2].dat,t[p*2+1].dat,t[p*2].rmax+t[p*2+1].lmax);
}*/
int change(int x,int v,int p)
{
if (t[p].l==t[p].r)
{
t[p].dat=v;
t[p].sum=v;
t[p].lmax=v;
t[p].rmax=v;
return 0;
}
int mid=(t[p].l+t[p].r)>>1;
if (x<=mid)
change(x,v,p<<1);
else
change(x,v,p*2+1);
t[p].sum=t[p<<1].sum+t[p*2+1].sum;
t[p].lmax=max(t[p<<1].lmax,t[p<<1].sum+t[p*2+1].lmax);
t[p].rmax=max(t[p*2+1].rmax,t[p*2+1].sum+t[p<<1].rmax);
t[p].dat=max(max(t[p<<1].dat,t[p*2+1].dat),t[p<<1].rmax+t[p*2+1].lmax);
}
//区间查询
//注意要查询的是区间最大连续字段和,所以返回结构体
node ask(int l,int r,int p){
if(l<=t[p].l&&r>=t[p].r){
return t[p];
}
int mid=t[p].l+(t[p].r-t[p].l)>>1;
if(r<=mid) return ask(l,r,p*2);
else if(l>mid) return ask(l,r,p*2+1);
else{//要查询的区间跨过mid
node a,b,c;//a,b分别为左、右儿子的最大子段和,c为跨过mid的最大子段和
a=ask(l,mid,p*2);b=ask(mid+1,r,p*2+1);
c.l=l,c.r=r;
c.sum=a.sum+b.sum;
c.dat=max(max(a.dat,b.dat),a.rmax+b.lmax);
c.lmax=max(a.lmax,a.sum+b.lmax);
c.rmax=max(b.rmax,a.rmax+b.sum);
return c;
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,n,1);
while(m--){
int k,x,y;
cin>>k>>x>>y;
if(k==1) {
if(x>y) swap(x,y);
cout<<ask(x,y,1).dat<<endl;
}
else change(x,y,1);
}
}