• 【数据结构】线段树


    线段树

    本文描述高级数据结构线段树的定义,并解决 点修改/区间求和 的问题




    结构与定义


    线段树的基本结构

    由图可知,线段树的每一个节点都代表着一段区间

    且同一层的节点(深度相同的节点)所表示的区间互不重叠

    所有叶子节点代表的区间左边界与右边界相同(叶子节点代表单个元素)


    普遍规定

    如果某个非叶子节点代表的区间包含元素个数为奇数

    则它的左儿子包含的元素个数比右儿子大 1

    在代码部分,非叶子节点表示区间为 [l,r]

    则左儿子为 [ l , (l+r)/2 ] ,右儿子为 [ (l+r)/2+1 , r ]

    除法计算向下取整

    如果需要用线段树对一段包含N个数的区间操作,则树一般需要开4N的空间以避免越界


    存储方式

    线段树以数组存储节点的值

    以层序遍历的方式自上而下,自左而右依次进行编号

    一般从0开始或者从1开始

    如果从0开始,则左儿子位置为2k+1,右儿子为2k+2

    如果从1开始,则左儿子位置为2k,右儿子为2k+1

    本文代码编号从1开始




    线段树实现方法演示

    该部分图片截自av6835937

    首先,对于一个区间大小为10的线段树,它的基本构造如下图所示

    如果区间大小为5,代入一个数组[1,5,4,1,6],那么线段树各个节点表示的元素为

    按照上述图示即可先进行建树

    然后先考虑点修改的操作

    如果要将a[2]的值修改为3,则从根节点到叶子节点的访问顺序为

    访问到叶子节点后,修改叶子节点的值,然后自下而上回溯每一个访问时经过的节点并依次进行更新,直到回溯到根节点

    因为只要叶子节点更新后,它的父节点就可以通过左儿子+右儿子来重新更新sum值,最后完成更新

    然后考虑区间查询操作

    传统方法中,查询区间元素之和的做法就是从[ l , r ]一个个加起来输出,这种O(n)的做法在遇到大数量的查询时运行速度完全无法满足要求

    因此,根据线段树的特点,我们可以通过预处理线段的和当作多个元素之和

    在上面建树时已经把线段和处理完了,所以接下来就需要考虑把符合的线段按最小个数取出来求和即可

    下图展示了求区间[3,5]的和的过程

    可以看到,在元素总数为5的线段树里,4 5这两个元素可以用 [4,5] 这个线段来表示

    所以3 4 5这三个元素和的值只需要访问[3,3] [4,5]这两个区间就能得到

    在查找时,需要以左边界3与右边界5作为媒介

    如果访问到的一个节点全部包含于[3,5],那么整个节点就可以直接拿过来作为答案,不需要再访问子树。图中访问到的节点[4,5]就满足这个情况。

    如果访问到的一个节点部分包含于[3,5],此时不能拿整个节点的值作为答案,需要分别访问其左子树和右子树。图中的根节点[1,5]与节点[1,3]就满足这个情况。

    如果访问到的一个节点不包含于[3,5],则无需继续考虑。图中访问到的节点[1,2]就满足这个情况。(在代码实现部分,这个节点需要访问到,但是因为对答案无贡献所以直接return)




    代码实现 - 以点修改/区间求和为例


    节点类型的构造 struct node

    typedef long long ll;
    const int MAXN=1e5+50;
    
    struct node
    {
    	int l,r;
    	ll sum;
    }tree[MAXN*4];
    

    建树 buildTree

    int ar[MAXN];
    
    scanf("%d",&n);
    for(i=1;i<=n;i++)
    	scanf("%d",&ar[i]);
    buildTree(1,1,n);
    
    void buildTree(int id,int l,int r)
    {
    	tree[id].l=l;
    	tree[id].r=r;
    	tree[id].sum=0;
    	if(l==r)
    		tree[id].sum=ar[l];
    	else
    	{
    		int mid=(l+r)>>1;
    		buildTree(id<<1,l,mid);
    		buildTree(id<<1|1,mid+1,r);
    		push_up(id);
    	}
    }
    

    按照递归的方法进行建树,main函数读入原数组ar,然后树从编号1开始创建

    建树函数传入共三个变量:当前要进行构造的节点id,这个节点代表的区间 l 与 r

    因为调用buildTree函数也就相当于tree结构体数组的初始化,所以要记得加tree[id].sum=0这一句

    如果当前节点就是叶子节点(即表示的区间的 l 与 r 相同),则直接把sum赋值成对应的ar数组内的值即可

    如果是非叶子节点,先获得当前节点表示的区间中间值mid=(l+r)>>1

    然后递归左儿子与右儿子建树

    这里写法运用了位运算(细微省时)

    id<<1 就是把id的二进制全部左移一位,相当于id*2

    id<<1|1 就是先把id的二进制全部左移一位,再对1做取或运算,相当于id*2+1

    当左儿子与有儿子建树完成时,再更新当前节点的sum值,即push_up函数


    更新节点值(向上传递) push_up

    void push_up(int id)
    {
    	tree[id].sum=tree[id<<1].sum+tree[id<<1|1].sum;
    }
    

    更新节点编号id的sum值为两子节点sum之和

    如果子节点发生了改变,则必须更新一遍父节点

    所以push_up函数会在建树buildTree和修改节点值update时调用


    修改节点值 update

    update(1,3,5); //把位置3的值修改为5
    
    void update(int id,int pos,ll val)
    {
    	int L=tree[id].l,R=tree[id].r;
    	if(L==R&&L==pos){
    		tree[id].sum=val;
    		return;
    	}
    	if(L<=pos&&pos<=R)
    	{
    		int mid=(L+R)>>1;
    		update(id<<1,pos,val);
    		update(id<<1|1,pos,val);
    		push_up(id);
    	}
    }
    

    同样的,update函数需要的三个参数中有一个是迭代参数id,指现在处理的节点id

    后两个参数固定不变,故也能修改为全局变量

    如果pos正好位于此时节点id表示的线段中,但是id节点不是叶子节点

    那么就处理节点id的左右儿子

    如果一个节点有被访问到,并且左右儿子都已经访问过了

    说明满足条件的那个子节点的值已经成为更新之后的值

    此时再更新自己,即push_up(id),然后退出函数,让栈中上一个函数继续执行

    这里就是查找路径的回溯,先更新叶子节点再一层层回溯父节点并进行更新

    如果满足条件L==R&&L==pos(或等价条件)时,说明此时id指向的是一个叶子节点,并且就是要修改的节点

    所以修改后直接return即可,就可以让路径开始回溯,更新父节点

    回溯到根节点时,说明这一次update操作就完成了


    查询区间和 query

    query(1,3,5); //查询区间[3,5]的和
    
    ll query(int id,int l,int r)
    {
    	int L=tree[id].l,R=tree[id].r;
    	if(l<=L&&R<=r)
    		return tree[id].sum;
    	int mid=(L+R)>>1;
    	ll res=0;
    	if(mid>=l)
    		res+=query(id<<1,l,r);
    	if(mid<r)
    		res+=query(id<<1|1,l,r);
    	return res;
    }
    

    与update函数类似的迭代

    如果满足l<=L&&R<=r,说明当前访问的节点id代表的区间完全包含于查询的区间内

    此时直接return tree[id].sum返回id节点的sum值即可

    如果节点id代表的区间只是部分包含于查询的区间内

    则需要迭代访问两个子节点

    如果满足mid>=l,说明节点id的左儿子会部分或全部包含于查询的区间[l,r],则可以用迭代左节点得到的值加入答案

    如果满足mid<r,说明节点id的右儿子会部分或全部包含于查询的区间[l,r],则可以用迭代右节点得到的值加入答案

    注意这里不能写成mid<=r,因为实际上判断的是mid+1<=r,等价于mid<r

    最后返回和加入答案即可


    代码调用部分

    这一部分采用了例题 HDU1166敌兵布阵 的输入要求:

    Add a b 使编号为a的元素值加上b

    Sub a b 使编号为a的元素值减去b

    Query a b 询问区间[a,b]之和

    End 结束程序

    char opr[10];
    while(1)
    {
    	scanf("%s",opr);
    	if(opr[0]=='E')
    		break;
    	scanf("%d%d",&a,&b);
    	if(opr[0]=='Q')
    		printf("%d
    ",query(1,a,b));
    	else if(opr[0]=='A')
    		update(1,a,b);
    	else if(opr[0]=='S')
    		update(1,a,-b);
    }
    




    完整程序

    #include<iostream>
    using namespace std;
    typedef long long ll;
    const int MAXN=50050;
    
    struct node
    {
    	int l,r;
    	ll sum;
    }tree[MAXN*4];
    
    int ar[MAXN];
    
    void push_up(int id)
    {
    	tree[id].sum=tree[id<<1].sum+tree[id<<1|1].sum;
    }
    
    void buildTree(int id,int l,int r)
    {
    	tree[id].l=l;
    	tree[id].r=r;
    	tree[id].sum=0;
    	if(l==r)
    		tree[id].sum=ar[l];
    	else
    	{
    		int mid=(l+r)>>1;
    		buildTree(id<<1,l,mid);
    		buildTree(id<<1|1,mid+1,r);
    		push_up(id);
    	}
    }
    
    void update(int id,int pos,ll val)
    {
    	int L=tree[id].l,R=tree[id].r;
    	if(L==R&&L==pos){
    		tree[id].sum+=val;
    		return;
    	}
    	if(L<=pos&&pos<=R)
    	{
    		int mid=(L+R)>>1;
    		update(id<<1,pos,val);
    		update(id<<1|1,pos,val);
    		push_up(id);
    	}
    }
    
    ll query(int id,int l,int r)
    {
    	int L=tree[id].l,R=tree[id].r;
    	if(l<=L&&R<=r)
    		return tree[id].sum;
    	int mid=(L+R)>>1;
    	ll res=0;
    	if(mid>=l)
    		res+=query(id<<1,l,r);
    	if(mid<r)
    		res+=query(id<<1|1,l,r);
    	return res;
    }
    
    int main()
    {
    	int T;
    	scanf("%d",&T);
    	while(T--){
    		int i,n;
    		scanf("%d",&n);
    		for(i=1;i<=n;i++)
    			scanf("%d",&ar[i]);
    		buildTree(1,1,n);
    		char opr[10];
    		while(1)
    		{
    			scanf("%s",opr);
    			if(opr[0]=='E')
    				break;
    			scanf("%d%d",&a,&b);
    			if(opr[0]=='Q')
    				printf("%d
    ",query(1,a,b));
    			else if(opr[0]=='A')
    				update(1,a,b);
    			else if(opr[0]=='S')
    				update(1,a,-b);
    		}
    	}
    	
    	return 0;
    }
    
  • 相关阅读:
    c#中的命名空间、类
    C#编写“hello,world”
    django1.9中manage.py的操作命令
    JS原生Ajax操作(XMLHttpRequest)
    Jquery百宝箱
    jQuery与Ajax入门
    JSON
    JSTL与EL表达式
    知识点整理
    Servlet与JSP进阶
  • 原文地址:https://www.cnblogs.com/stelayuri/p/12520441.html
Copyright © 2020-2023  润新知