algorithm: 线段树-实现(递归和非递归)

1.定义

线段树是一种二叉搜索树
线段树将一段区间[a,b]划分为一些单位区间,每个单位区间对应线段树的一个叶子节点(元节点)。
对于线段树中的每一个非叶子节点[a,b],它的左儿子区间[a,(a+b)/2],右儿子区间[(a+b)/2+1,b]
给张图直观理解下
在这里插入图片描述
用线段树统计的东西必须符合区间加法,即[a,b]区间的结果可以由[a,(a+b)/2]和[(a+b)/2+1,b]相“加”获得。
符合区间加法的例子有:
数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和
最大公因数(GCD)——总GCD = gcd( 左区间GCD , 右区间GCD );
最大值——总最大值=max(左区间最大值,右区间最大值)
不符合区间加法的例子:
众数——只知道左右区间的众数,没法求总区间的众数
01序列的最长连续零——只知道左右区间的最长连续零,没法知道总的最长连续零

线段树分解:递归的将区间[l,r],分割为[l,m]和[m+1,l],其中m=(l+r)/2,假设根的高度为1,树的高度为 l o g 2 ( n − 1 ) + 2 ( n > 1 ) log_2(n-1)+2(n>1) log2(n1)+2(n>1),对于每个n树的分解是唯一的,所以n相同,树结构相同,为可持久化线段树提供了基础。

理论

  • 1.点修改:线段树修改某一点,只需要修改每一层的一个点,修改的次数最大值等于树的高度
  • 2.区间查询:n>=3时,一个[1,n]的线段树可以将[1,n]的任意子区间[L,R]分解为不超过 2 l o g 2 ( n − 1 ) 2log_2(n-1) 2log2(n1)个子区间,对该结论证明感兴趣的可以参考本文最后的引用处。
  • 3.线段树的区间修改:线段树的区间修改也是将区间分成子区间,但是要加一个标记,称作懒惰标记。标记的含义:本节点的统计信息已经根据标记更新过了,但是本节点的子节点仍需要进行更新。
  • 4.线段树的存储结构:线段树是用数组来模拟树形结构,对于每一个节点R,左子节点为 2R (一般写作R<<1), 右子节点为 2R+1(一般写作R<<1|1)
    然后以1为根节点,所以,整体的统计信息是存在节点1中的,节点0通常不做使用。
    牢记左子树和右子树与根节点的索引关系 [0,1,2,3,4,5,6,7,8,9],这里构建一个[1,5]的线段树,可以看下左子树和右子树的索引关系

2.实现

线段树的实现有两种方式:递归和非递归,递归从上到下,根节点到叶子节点;非递归从下到上,从叶子节点到根节点

递归实现

自上而下递归构建线段树

#define n 10007

int sum[n<<2], add[n<<2]; // add为懒惰标记
int a[n], num;

void pushUp(int rt){sum[rt]=sum[rt<<1]+sum[rt<<1|1];}
//自上而下构建线段树
void build(int l, int r, int rt){
    if(l==r){
        sum[rt] = a[l];
        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){
    if(l==r){//叶子节点
        sum[rt] += c;
        return;
    }
    int m = (r+l)>>1;
    if(L<=m) update(L, c, l, m, rt<<1);
    else update(L, c, m+1, r, rt<<1|1);
    pushUp(rt);
}

下推标记,递归线段树是自上而下,该函数是计算add数组存储的子节点变动值,也就是将标记值下推

// ln rn分别为左端点和右端点
void pushDown(int rt, int ln, int rn){
    if(add[rt]){
        //下推标记
        add[rt<<1] += add[rt];
        add[rt<<1|1] += add[rt];
        //修改子节点的值
        sum[rt<<1] += add[rt]*ln;
        sum[rt<<1|1] += add[rt]*rn;
        add[rt] = 0; // 清除当前标记
    }
}

区间修改 a[L,R] += c

void update(int L, int R, int c, int l, int r, int rt){
    if(L<=l && r<=R){ //区间位于[L,R]内
        sum[rt] += c*(r-l+1);
        add[rt] += c;//add标记,表示本区间的sum正确,子区间的sum需要更新
        return;
    }
    int m = (l+r)>>2;
    pushDown(rt, m-l+1, r-m);//下推之前的标记,更新值
    if(L<=m) update(L, R, c, l, m, rt<<1);
    if(R>m) update(L, R, c, m+1, r, rt<<1|1);
    pushUp(rt);
}

区间查询

int query(int L, int R, int l, int r, int rt){
    if(l>=L && r<=R) return sum[rt];
    int m = (l+r)>>1;
    pushDown(rt, m-l+1, r-m);
    int res=0;
    if(R>m) res += query(L, R, m+1, r, rt<<1|1);
    if(L<=m) res += query(L, R, l, m, rt<<1);
    return res;
}
非递归实现

非递归实现这里采用一种简洁的方式,假设原数组长度为n,则定义一个长度为2n的数组存储线段树
非递归线段树构建、点位修改和区间查询都比较简洁,所以能非递归尽量采用非递归

  • 这里我采用的非递归构建方式,左节点索引始终为偶数,右节点索引始终为奇数
// n为原数组的长度
void build() {
    for(int i=0;i<n;i++){
        for(int i=0;i<n;i++){
            sum[i+n] = a[i];
        }
        for(int i=n-1;i>0;i--){
            sum[i] = sum[i<<1]+sum[i<<1|1];
            add[i]=0;
        }
    }
}
//点修改 a[L]+=c
void update(int L, int c){
    for(int i=L+n;i>0;i>>=1){
        sum[i] += c;
    }
}
//点修改下的区间查询
int query(int L, int R){
    int res=0;
    for(int l=L+n, r=R+n;l^r^1;l>>=1, r>>=1){
        if(l&1) res += sum[l++];
        if(~r&1) res += sum[r--];
    }
    return res;
}

非递归的区间修改和区间查询编写是较复杂,原因在于非递归是自下而上,很难在一个区间传递变更值,这里借助add标记来完成

//区间修改
void update(int L, int R, int c){
    int l=n+L, r=n+R, Ln=0, Rn=0, x=1;
    for(;l^r^1;l>>=1, r>>=1, x<<=1){
        sum[l] += c*Ln; // 加上舍弃的节点的值
        sum[r] += c*Rn;
        if(l&1) { // 左端点是右子节点时,舍弃该节点
            add[l]+=c; // 标记该节点,该节点子节点需要加c
            sum[l++]+=c*x; // 更新该节点的值
            Ln+=x;
        }
        if(~r&1){
            add[r]+=c;
            sum[r--]+=c*x;
            Rn+=x;
        }
    }
    for(;l;l>>=1,r>>=1){
        sum[l]+=c*Ln;
        sum[r]+=c*Rn;
    }
}
// 查询思路与区间更新一致类似,但不需要更新sum值
int query(int L, int R){
    int l=n+L,r=n+R,Ln=0,Rn=0,x=1;
    int res=0;
    for(;l^r^1;l>>=1,r>>=1,x<<=1){
        if(add[l]) res+=add[l]*Ln;
        if(add[r]) res+=add[r]*Rn;
        if(l&1) {
            res+=sum[l++];
            Ln+=x;
        }
        if(~r&1) {
            res+=sum[r--];
            Rn+=x;
        }
    }
    for(;l;l>>=1,r>>=1){
        res+=add[l]*Ln;
        res+=add[r]*Rn;
    }
    return res;
}

以上代码根据借鉴的思路写的,还未测试,有问题欢迎指正。

线段树详解 (原理,实现与应用)
非递归线段树区间修改区间求和的两种实现(以POJ 3468为例)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值