线段树

线段树的由来

一般来说,对于[1,n]的操作,修改与统计,复杂度是O(n),但是用先单数优化,可以达到O(log(n))
比如

题目一:
10000个正整数,编号1到10000,用A[1],A[2],A[10000]表示。
修改:无
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

方法一:对于统计L,R ,需要求下标从L到R的所有数的和,从L到R的所有下标记做[L..R],问题就是对A[L..R]进行求和。
这样求和,对于每个询问,需要将(R-L+1)个数相加。
方法二:更快的方法是求前缀和,令 S[0]=0, S[k]=A[1..k] ,那么,A[L..R]的和就等于S[R]-S[L-1],
这样,对于每个询问,就只需要做一次减法,大大提高效率。

题目二:
10000个正整数,编号从1到10000,用A[1],A[2],A[10000]表示。
修改:1.将第L个数增加C (1 <= L <= 10000)
统计:1.编号从L到R的所有数之和为多少? 其中1<= L <= R <= 10000.

方法一:直接修改第L个数
方法二:把L到R的所有值都要加上C

所以,从上面可以看出来,普通的方法对于修改比较快,但求和比较慢。前缀和求和比较快,但修改比较慢
所以,基于此问题,将修改和统计集与一体的线段树就诞生了

线段树介绍

线段树是一种二叉树结构,对于一个线段或区间,可以用一个二叉树来表示

假设有编号1到n的n个点,每个点都存了一些信息,用[L,R]来表示下标从L到R的这些点
线段树的作用就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(log(n))

线段树的原理,就是,将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为
少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。

由此看出,用线段树统计的东西,必须符合区间加法,否则,不可能通过分成的子区间来得到[L,R]的统计结果。

符合区间加法的例子:

  • 数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和
  • 最大公因数(GCD)——总GCD = gcd( 左区间GCD , 右区间GCD );
  • 最值——总最大值=max(左区间最大值,右区间最大值)

不符合区间加法的例子:

  • 众数——只知道左右区间的众数,没法求总区间的众数
  • 01序列的最长连续零——只知道左右区间的最长连续零,没法知道总的最长连续零

如何划分区间

对于区间[1,n]分成不超过4n个子区间,对于每个子区间,记录一段连续数字的和,
知乎,任意给定区间[L,R],线段树再上述子区间种选择约2
log(R-L+1)个拼成区间[L,R]
如果对于A[L]+=C的操作,线段树的子区间里,越有log(n)个包含了L,所以只需要修改log(n)个子区间

首先是讲原始子区间的分解,假定给定区间[L,R],只要L < R ,线段树就会把它继续分裂成两个区间。
首先计算 M = (L+R)/2,左子区间为[L,M],右子区间为[M+1,R],然后如果子区间不满足条件就递归分解。

线段树是用数组来模拟树形结构,对于每一个节点R ,左子节点为 2R (一般写作R<<1)右子节点为 2R+1\((一般写作R<<1|1)\)
然后以1为根节点,所以,整体的统计信息是存在节点1中的。

以区间[1..13]的分解为例,分解结果见下图

为什么开成tree[4*n]
因为所需要存的是完全二叉树的所有的数据,那么为\(2^{log_{2}(n)+1}-1\)

对于[2,12]呢

如何统计区间

假设这13个数为1,2,3,4,1,2,3,4,1,2,3,4,1. 在区间之后标上该区间的数字之和:

如果要计算[2,12]
[2,12]=[2]+[3,4]+[5.7]+[8,10]+[11,12]=29

构建的线段树是,是一个完全二叉树的存储方式
31 16 15 10 6 7 8 3 7 3 3 5 2 7 1 1 2 3 4 1 2 0 0 4 1 0 0 3 4 0 0

如何点修改

如果把A[6]+=7,那么要修改[6],[5,6],[5,7],[1,7],[1,13]

存储结构

可以用结构体
但最好用数组来实现
定义数组的空间为A[4n]

代码

定义数组

const int maxn=10005;
int sum[maxn<<2];//sum求和,开四倍空间
int A[maxn],n;//存原数组[1,n]

建立线段树

void pushup(int rt){//进行更新节点信息
    sum[rt]=sum[rt<<1]+sum[rt<<1|1]//非叶节点的值=左子结点+右子结点
}
void build(int l,int r,int rt){//[l,r]表示当前节点区间,rt表示当前节点的实际存储位置 
    if(l==r){//若到达叶结点
        sum[rt]=A[l];//储存a数组的值
        return;
    }
    int m=(l+r)>>1;
    build(l,m,rt<<1);//左递归构建树
    build(m+1,r,rt<<1|1);
    pushup(rt);//更新
}

点修改

A[l]+=c;

void update(int L,int c,int l,int r,int rt){//[l,r]为区间,rt是当前的节点编号
    if(l==r){//到达叶结点。修改叶结点的值
        sum[rt]+=c;
        return;
    }
    int m=(l+r)>>1;
    if(L<=m)update(L,c,l,m,rt<<1);
    else update(L,c,m+1,r,rt<<1|1);
    pushup(rt);//子结点更新了,所以本结点也需要更新
}

区间修改

加上了一个惰性标记


区间查询

询问A[l,r]的和

int query(int L,int R,int l,int r,int rt){//[L,R]表示操作区间,[l,r]表示当前区间,rt表示当前结点
    if(L<=l&&r<=R){//如果在区间内就直接返回
        return sum[rt];
    }
    int m=(l+r)>>1;
    int ans=0;
    if(L<=m)ans+=query(L,R,l,m,rt<<1);//左子区间与[L,R]有重叠,递归
    if(R>m)ans+=query(L,R,m+1,r,rt<<1|1);//右子区间与[L,R]有重叠,递归
    return ans; 
}

利用一维数组储存的模板,点修改,区间查询

命令
1更新 第l个点加C
0 查询L到R的和
输入

3 2
1 2 3
1 2 0
2 1 3
#include<iostream>
#include<cstdio>
#include<algorithm>
#define il inline
#define ll long long
#define debug printf("%d %s\n",__LINE__,__FUNCTION__)
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
using namespace std;
const int N=200005;
ll n,m,sum[N<<2];
ll R,L,C;
il ll read(){//快速读入数字
    char ch=getchar();ll x=0,f=1;
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
il void pushup(int rt){
    sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
il void build(int l,int r,int rt){
    if(l==r){
        sum[rt]=read();
        return;
    }
    int m=(l+r)>>1;
    build(lson),build(rson);
    pushup(rt);
}
il void update(int l,int r,int rt){
    if(l==r){sum[rt]+=C;return;}
    ll m=(l+r)>>1;
    if(L<=m)update(lson);//更新在左子树
    else update(rson);//更新在右子树
    pushup(rt);//子结点已经更新,该结点也需要更新
}
il ll query(int l,int r,int rt){
    if(L<=l&&R>=r)return sum[rt];
    int m=(l+r)>>1;
    ll ret=0;
    if(L<=m)ret+=query(lson);
    if(m<R)ret+=query(rson);
    return ret;
}
int main(){
    while(~scanf("%d%d",&n,&m)){
        build(1,n,1);
        while(m--){
            ll u=read();
            if(u==1){
                L=read(),C=read();
                update(1,n,1);
            }
            else {
                L=read(),R=read();
                printf("%lld\n",query(1,n,1));
            }
        }
    }

    return 0;
}


区间修改中的取根号

输入

4
1 100 5 5
5
1 1 2
2 1 2
1 1 2
2 2 3
1 1 4
#include<bits/stdc++.h>
#define il inline
#define ll long long
#define debug printf("%d %s\n",__LINE__,__FUNCTION__)
#define lson l,m,rt<<1//左儿子
#define rson m+1,r,rt<<1|1//右儿子
using namespace std;
const int N=100005;
ll n,m,sum[N<<2],flag[N<<2];
ll R,L,C;
il ll read(){//快速读入数字
    ll a=0;char x=getchar();bool f=0;
    while((x<'0'||x>'9')&&x!='-')x=getchar();
    if(x=='-')x=getchar(),f=1;
    while(x>='0'&&x<='9')a=a*10+x-48,x=getchar();
    return f?-a:a;
}
il void pushup(int rt){
    sum[rt]=sum[rt<<1]+sum[rt<<1|1];
    flag[rt]=flag[rt<<1]&flag[rt<<1|1];
}

il void build(int l,int r,int rt){
    if(l==r){
        sum[rt]=read();
        if(sum[rt]<=1)flag[rt]=1;
        return;
    }
    int m=(l+r)>>1;
    build(lson),build(rson);
    pushup(rt);
}
il void update(int l,int r,int rt){
    if(flag[rt]==1)return;
    if(l==r){
        sum[rt]=floor(sqrt(sum[rt]));
        if(sum[rt]<=1)flag[rt]=1;
        return;
    }
    int m=(l+r)>>1;
    if(L<=m)update(lson);
    if(m<R)update(rson);
    pushup(rt);
}
il ll query(int l,int r,int rt){
    if(L<=l&&R>=r)return sum[rt];
    //pushdown(l,r,rt);
    int m=(l+r)>>1;
    ll ret=0;
    if(L<=m)ret+=query(lson);
    if(m<R)ret+=query(rson);
    return ret;
}
int main(){
    n=read();
    build(1,n,1);
    m=read();
    while(m--){
        ll u=read();
        if(u==2){
            L=read(),R=read();
            update(1,n,1);
        }
        else {
            L=read(),R=read();
            printf("%lld\n",query(1,n,1));
        }
    }
    return 0;
}

利用一维数组储存的模板,区间修改,区间查询

大佬写法

#include<bits/stdc++.h>
#define il inline
#define ll long long
#define debug printf("%d %s\n",__LINE__,__FUNCTION__)
#define lson l,m,rt<<1//左儿子
#define rson m+1,r,rt<<1|1//右儿子
using namespace std;
const int N=100005;
ll n,m,sum[N<<2],add[N<<2];
ll R,L,C;
il ll read(){//快速读入数字
    ll a=0;char x=getchar();bool f=0;
    while((x<'0'||x>'9')&&x!='-')x=getchar();
    if(x=='-')x=getchar(),f=1;
    while(x>='0'&&x<='9')a=a*10+x-48,x=getchar();
    return f?-a:a;
}
il void pushup(int rt){
    sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
il void pushdown(int l,int r,int rt){
    ll m=r-l+1;
    if(add[rt]){
        add[rt<<1]+=add[rt];
        add[rt<<1|1]+=add[rt];
        sum[rt<<1]+=add[rt]*(m-(m>>1));
        sum[rt<<1|1]+=add[rt]*(m>>1);
        add[rt]=0;
    }
}
il void build(int l,int r,int rt){
    add[rt]=0;
    if(l==r){
        sum[rt]=read();
        return;
    }
    int m=(l+r)>>1;
    build(lson),build(rson);
    pushup(rt);
}
il void update(int l,int r,int rt){
    if(L<=l&&R>=r){
        add[rt]+=C;
        sum[rt]+=(ll)C*(r-l+1);
        return ;
    }
    pushdown(l,r,rt);
    int m=(l+r)>>1;
    if(L<=m)update(lson);
    if(m<R)update(rson);
    pushup(rt);
}
il ll query(int l,int r,int rt){
    if(L<=l&&R>=r)return sum[rt];
    pushdown(l,r,rt);
    int m=(l+r)>>1;
    ll ret=0;
    if(L<=m)ret+=query(lson);
    if(m<R)ret+=query(rson);
    return ret;
}
int main(){
    n=read(),m=read();
    build(1,n,1);
    while(m--){
        ll u=read();
        if(u==1){
            L=read(),R=read(),C=read();
            update(1,n,1);
        }
        else {
            L=read(),R=read();
            printf("%lld\n",query(1,n,1));
        }
    }
    return 0;
}

利用结构体储存的模板

#include<bits/stdc++.h>
#define ll(x) (x<<1)
#define rr(x) (x<<1|1)
using namespace std;
const int N=400000+5;
typedef long long lol;

lol n, m;
lol w[N];

struct seg_tree{
  lol sum, l, r, lazy;
}t[N];

lol gi(){
  lol ans = 0 , f = 1; char i=getchar();
  while(i<'0'||i>'9'){if(i=='-')f=-1;i=getchar();}
  while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();}
  return ans * f;
}

void up(lol root){
  t[root].sum = t[ll(root)].sum + t[rr(root)].sum;
}

void build(lol root,lol l,lol r){
  int mid = l+r>>1;
  t[root].l = l , t[root].r = r;
  if(l == r){
    t[root].sum = w[l];
    return;
  }
  build(ll(root),l,mid);
  build(rr(root),mid+1,r);
  up(root);
}

void pushdown(lol root){
  lol mid = t[root].l + t[root].r >> 1;
  t[ll(root)].lazy += t[root].lazy;
  t[rr(root)].lazy += t[root].lazy;
  t[ll(root)].sum += t[root].lazy*(mid-t[root].l+1);
  t[rr(root)].sum += t[root].lazy*(t[root].r-mid);
  t[root].lazy = 0;
}

void updata(lol root,lol l,lol r,lol val){
  lol mid = t[root].l+t[root].r>>1;
  if(l<=t[root].l && t[root].r<=r){
    t[root].sum += val * (t[root].r-t[root].l+1);
    t[root].lazy += val;
    return;
  }
  if(t[root].lazy) pushdown(root);
  if(l <= mid) updata(ll(root),l,r,val);
  if(mid < r) updata(rr(root),l,r,val);
  up(root);
}

lol query(lol root,lol l,lol r){
  if(l<=t[root].l && t[root].r<=r) return t[root].sum;
  if(r<t[root].l || t[root].r<l) return 0;
  if(t[root].lazy) pushdown(root);
  return query(ll(root),l,r)+query(rr(root),l,r);
}

int main(){
  lol f, x, y, val; n = gi(); m = gi();
  for(lol i=1;i<=n;i++) w[i] = gi();
  build(1,1,n);
  for(lol i=1;i<=m;i++){
    f = gi(); x = gi(); y = gi();
    if(f == 1) val = gi() , updata(1,x,y,val);
    else printf("%lld\n",query(1,x,y));
  }
  return 0;
}

我的疑问

线段树每次查询后树的数组值都会发生改变,那么如何保证每次查询的结果是对的???

举个例子
下标为1到6的6个数,值为1 3 2 4 6 5

线段树为
21 6 15 4 2 10 5 1 3 0 0 4 6 0 0

我想错了,事实上每次查询都不会变,从代码可以看出,最后返回的是ans,之前变化是因为我加了修改功能。

pushdown()是干嘛的?为什么区间修改时要加pushdown(),而且在区间修改和点修改中有一行判断语句不一样

线段树不是维护一段区间有一个数值吗,可以这个代码只能做到数值必须精确到每个数,然后才能支撑到区间

如何做到一个区间维护一段数值而他的子区间可能不存在

posted @ 2019-08-26 14:19  Emcikem  阅读(252)  评论(0编辑  收藏  举报