线段树【原理及模板】

改编自某大佬:https://blog.csdn.net/zearot/article/details/52280189

https://www.cnblogs.com/AC-King/p/7789013.html

跟朵朵大佬:https://blog.csdn.net/zhangxiaoduoduo/article/details/81557576

一、为什么需要线段树?

方法一:对于统计L, R, 需要求下标从L到R的所有数的和,从L到R的所有下标记作[ L...R ],  问题就是对A[ L...R ]进行求和。

这样求和,对于每个询问,需要将(R-L+1)个数相加。

方法二:更快的方法的是求前缀和,令S[0] = 0, S[k] = A[1]+...A[k],  那么,A[L]+..A[R]的和就等于S[R]-S[L-1]。

这样求和,对于每个询问,就只需要做一次减法,大大提高效率。


再使用方法二的话,假如A[L]+=C之后,S[L],S[L+1]...S[R]都需要增加C,全部都要修改,如图。

!!!!!表中第三行第一列表示为A[L]+...+A[R]

从上表可以看出,方法一修改快,求和慢。方法二求和快,修改慢。

而线段树就是一种集修改和求和都快于一体的结构了。

二、线段树的点修改

问题二就是典型的线段树点修改。

线段树先将区间[1...10000]分成不超过4*10000个子区间,对于每个子区间记录一段连续数字的和。

之后,任意给定区间[L,R], 线段树在上述子区间中选择约2*log(R-L+1) 个拼成区间[L,R]。

如果A[L]+=C即向A[L]上加上某一数字,线段树的子区间中,约有log2(10000) 个包含了L,所以需要修改log2(10000)个。

于是,使用线段树的话

A[L]+=C需要修改log2(10000)个元素

求和A[L...R]需要修改2*log2(R-L+1)<=2*log2(10000)个元素。

log2(10000)<14所以相对来说线段树的修改和求和都比较快。


问题一:开始的子区间是怎么分的

首先先说原始子区间如何分解,假定给定区间[L,R],只要L<R,线段树就会把它继续分裂成两个区间。

首先计算M = (L+R)/2,左子区间为[L,M],右子区间为[M+1,R],然后如果子区间不满足条件就会递归分解。

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

问题二:给定区间[L,R],如何分解成上述给定区间?

对于给定区间[2,12]要如何分解成上述区间呢?

分解方法一:自下而上合并——利于理解

先考虑树的最下层,将所有在区间[2,12]内的点选中,然后,若相邻的点的直接父节点是同一个,那么就用这个父节点代替这两个结点(父节点在上一层)。这样操作后,本层最多剩下两个结点。若最左侧被选中的结点是他父节点的右子树,那么这个结点会被剩下那么这个节点会被剩下。中间的所有节点都被父节点取代。对最下层处理完之后,考虑它的上一层,继续进行同样的处理。

 

 

下图为n=13的线段树,区间[2,12],按照上面的叙述进行操作的过程图:

由图可以看出:在n=13的线段树中,[2,12]=[2] + [3,4] + [5,7] + [8,10] + [11,12] 。

分解方法二:自上而下分解——利于计算

首先对于区间[1,13],计算(1+13)/2 = 7,于是将区间[2,12]“切割”成了[2,7]和[8,12]。

其中[2,7]处于节点[1,7]的位置,[2,7] < [1,7] 所以继续分解,计算(1+7)/2 = 4, 于是将[2,7] 切割成[2,4]和[5,7]。

[5,7]处于节点[5,7]的位置,所以不用继续分解,[2,4]处于区间[1,4]的位置,所以继续分解成[2]和[3,4]。

最后【2】 < 【1,2】,所以计算(1+2)/2=1 ,将【2】用1切割,左侧为空,右侧为【2】

当然程序是递归计算的,不是一层一层计算的,上图只表示计算方法,不代表计算顺序。

问题三:如何进行区间统计?

假设这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  = 2 + 7 + 6 + 7 + 7

计算5个数的和就可以算出[2,12]的值。

问题四:如何进行点修改?

假设把A[6]+=7 ,看看哪些区间需要修改?[6],[5,6],[5,7],[1,7],[1,13]这些区间全部都需要+7.其余所有区间都不用动。

于是,这颗线段树中,点修改最多修改5个线段树元素(每层一个)。

下图中,修改后的元素用蓝色表示。

问题五:存储结构是怎样的?

线段树是一种二叉树,当然可以像一般的树那样写成结构体,指针什么的。
但是它的优点是,它也可以用数组来实现树形结构,可以大大简化代码。
数组形式适合在编程竞赛中使用,在已经知道线段树的最大规模的情况下,直接开足够空间的数组,然后在上面建立线段树。
简单的记法: 足够的空间 = 数组大小n的四倍。 
实际上足够的空间 =  (n向上扩充到最近的2的某个次方)的两倍。
举例子:假设数组长度为5,就需要5先扩充成8,8*2=16.线段树需要16个元素。如果数组元素为8,那么也需要16个元素。
所以线段树需要的空间是n的两倍到四倍之间的某个数,一般就开4*n的空间就好,如果空间不够,可以自己算好最大值来省点空间。
怎么用数组来表示一颗二叉树呢?假设某个节点的编号为v,那么它的左子节点编号为2*v,右子节点编号为2*v+1。
然后规定根节点为1.这样一颗二叉树就构造完成了。通常2*v在代码中写成 v<<1 。 2*v+1写成 v<<1|1 。

综述

假设有编号从1到n的n个点,每个点都存了一些信息,用[L,R]表示下标从L到R的这些点。

线段树的用处就是,对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(log2(n)).

线段树的原理,就是,将[1,n]分解成若干特定的子区间(数量不超过4*n),然后,将每个区间[L,R]都分解为

少量特定的子区间,通过对这些少量子区间的修改或者统计,来实现快速对[L,R]的修改或者统计。

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

符合区间加法的例子:

数字之和——总数字之和 = 左区间数字之和 + 右区间数字之和

最大公因数(GCD)——总GCD = gcd( 左区间GCD , 右区间GCD );

最大值——总最大值=max(左区间最大值,右区间最大值)

不符合区间加法的例子

众数——只知道左右区间的众数,没法求总区间的众数

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

 

一个问题,只要能化成对一些连续点的修改和统计问题,基本就可以用线段树来解决了,由于点的信息可以千变万化,所以线段树是一种非常灵活的数据结构,可以做的题的类型特别多,只要会转化。

线段树当然是可以维护线段信息的,因为线段信息也是可以转换成用点来表达的(每个点代表一条线段)。

定义:

#define maxn 100007 //元素总个数
int Sum[maxn<<2]; //Sum求和,开四倍空间
int A[maxn],n; //存原数组下标[1,n]

建树:

//PushUp函数更新结点信息,这里是求和
void PushUp(int rt)
{
    Sum[rt] = Sum[rt<<1]+Sum[rt<<1|1];  //该结点的和等于左子结点的和加右子节点的和;<<为位运算,<<1:乘以2;<<1|1:乘以2+1;
}

//Build函数建立线段树
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;//定义m等于该结点下一层结点的中间值(从哪个地方分成的两个结点)
    
    //左右递归,  分别继续往下递归  使其到达叶子结点;
    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是当前节点编号  l,r表示当前节点区间,rt表示当前节点编号,C表示所加的数,L表示所加的结点;
{
    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); //子节点更新了,所以本节点也需要更新信息;
}

区间修改:

void Update(int L, int R, int C, int l, int r, int rt)
{
//L,R表示操作区间,l,r表示当前节点区间,r,t表示当前节点编号;
    if(L<=l&&r<=R)//如果本区间完全在操作区间[L,R]以内
    {
        Sum[rt]+=C*(r-l+1);//更新数字和,向上保持正确
        Add[rt]+=C;//增加Add标记,表示本区间的Sum正确,子区间的Sum仍需要根据Add的值来调整
        return ;
    }
    int m=(l+r)>>1;
    PushDown(rt,m-l+1, r-m);//下推标记

    //这里判断左右子树跟[L,R]有无交集,有交集才递归
    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); //更新本节点信息
}

区间查询(本题为求和):

首先是下推标记的函数:

void PushDown(int rt,int ln,int rn)
{//ln,rn为左子树,右子树的数字数量。 
	if(Add[rt])
        {
		//下推标记 
		Add[rt<<1]+=Add[rt];
		Add[rt<<1|1]+=Add[rt];

		//修改子节点的Sum使之与对应的Add相对应 
		Sum[rt<<1]+=Add[rt]*ln;
		Sum[rt<<1|1]+=Add[rt]*rn;

		//清除本节点标记 
		Add[rt]=0;
	}
}

然后是区间查询的函数:

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;
  
   //左子区间:[l,m] 右子区间:[m+1,r]  求和区间:[L,R]
    PushDown(rt,m-l+1,r-m);
   //累加答案
    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;  
}   

函数调用:

	//建树 
	Build(1,n,1); 
	//点修改
	Update(L,C,1,n,1);
	//区间修改 
	Update(L,R,C,1,n,1);
	//区间查询 
	int ANS=Query(L,R,1,n,1);

 

模板一:

单点更新,单点查询:

int a[maxn];
int ans_min,ans_max,ans_sum;
struct node{
    int l,r;
    int maxx,minx,sum;
}tree[maxn<<2];
void build(int o,int l,int r)
{
    tree[o].l = l;
    tree[o].r = r;
    if(l == r){
        tree[o].maxx = tree[o].minx = tree[o].sum = a[l];
        return ;
    }
    int m = MID(l,r);
    int  lc = lson(o),rc = rson(o);
    build(lc,l,m);
    build(rc,m+1,r);
    tree[o].maxx = max(tree[lc].maxx, tree[rc].maxx);
    tree[o].minx = min(tree[lc].minx, tree[rc].minx);
    tree[o].sum = tree[lc].sum + tree[rc].sum;
}
void update(int o,int p,int v)
{
    if(tree[o].l == tree[o].r)
    {
        tree[o].maxx = v;  //视情况而定
        tree[o].minx = v;
        tree[o].sum = v;
        return ;
    }
    int m = MID(tree[o].l,tree[o].r);
    int lc = lson(o),rc = rson(o);
    if(p<=m) update(lc,p,v);
    else update(rc,p,v);
    tree[o].maxx = max(tree[lc].maxx, tree[rc].maxx);
    tree[o].minx = min(tree[lc].minx, tree[rc].minx);
    tree[o].sum = tree[lc].sum + tree[rc].sum;
}
void query_init()//查询前,将全局变量初始化
{
    ans_max = -INF;
    ans_min = INF;
    ans_sum = 0;
}
void query(int o,int l,int r)
{
    if(tree[o].l == l && tree[o].r == r)
    {
        ans_max = max(ans_max, tree[o].maxx);
        ans_min = min(ans_min, tree[o].minx);
        ans_sum += tree[o].sum;
        return;
    }
    int m = MID(tree[o].l,tree[o].r);
    int lc = lson(o),rc = rson(o);
    if(r<=m) 
        query(lc,l,r);
    else if(l>m) 
        query(rc,l,r);
    else
    {
        query(lc,l,m);
        query(rc,m+1,r);
    }
}

 

模板2:

区间更新,区间查询:

LL a[maxn];
struct node{
    int l,r;
    LL sum,inc;
}tree[maxn<<2];
void build(int o,int l,int r)
{
    tree[o].l = l;
    tree[o].r = r;
    tree[o].inc = 0;
    if(l == r)
    {
        tree[o].sum = a[l];
        return ;
    }
    int m = MID(l,r);
    int  lc = lson(o),rc = rson(o);
    build(lc,l,m);
    build(rc,m+1,r);
    tree[o].sum = tree[lc].sum+tree[rc].sum;
}
void update(int o,int l,int r,LL val)
{
    if(tree[o].l == l && tree[o].r == r)
    {
        tree[o].inc += val;
        return ;
    }
    tree[o].sum += val*(r-l+1);
    int m = MID(tree[o].l,tree[o].r);
    int lc = lson(o),rc = rson(o);
    if(r<=m) 
        update(lc,l,r,val);
    else if(l>m) 
        update(rc,l,r,val);
    else
    {
        update(lc,l,m,val);
        update(rc,m+1,r,val);
    }
}
LL ans_sum;
LL query(int o,int l,int r)
{
    if(tree[o].l == l && tree[o].r == r)
    {
        return tree[o].sum + tree[o].inc *(r-l+1);
    }
    tree[o].sum += (tree[o].r-tree[o].l+1)*tree[o].inc;
    int m = MID(tree[o].l,tree[o].r);
    int lc = lson(o),rc = rson(o);
    update(lc,tree[o].l,m,tree[o].inc);
    update(rc,m+1,tree[o].r,tree[o].inc);  //只更新到下一层
    tree[o].inc = 0;  //清除标记
    if(r<=m) 
        ans_sum = query(lc,l,r);
    else if(l>m) 
        ans_sum = query(rc,l,r);
    else
    {
        ans_sum = query(lc,l,m)+query(rc,m+1,r);
    }
    return ans_sum;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值