线段树炒鸡无敌简单入门法(lazy在另外的一篇)

最近在学习线段树这个东东,有点难,但是学完之后确实收获挺多
第一:线段树的优势
对于我们平常对于一段区间的存储,删改,查询区间的值,我们一般会选择用普通的一维数组去存储
如下面:我们输入n个数,求第s个到第t个数的和

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
    
    
	int n.s,t;
	int sum=0;
	cin>>n>>s>>t;
	int a[n+1];
	for(int i=0;i<n;i++)
		cin>>a[i];
	for(int i=s-1;i<=t-1;i++)
		sum+=a[i];
	cout<<sum<<endl;
}

在这段代码中,我们修改某一个值的时间复杂度是O(1),我们求一段区间的和的时间复杂度是O(n);
除此外,我们还会选择前缀和的方式,来应对和的输出

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
    
    
	int n,s,t,x;
	cin>>n>>s>>t;
	int a[n+1];
	a[0]=0;
	for(int i=1;i<=n;i++)
	{
    
    
		cin>>x;
		a[i]=a[i-1]+x;
	}
	cout<<a[t]-a[s]<<endl;
}

对于上面的代码,其对于求某一段和的时间复杂度是O(1)
但是对于修改某一个数值后的修改的时间复杂度是O(n)。
综上所述,对于对一段数值有着重复求和和修改值的操作时,无论是普通的一维数组还是前缀和的形式,都是有着相当大的时间复杂度。
那么有没有两种操作的时间复杂度都很低的算法,那就是我们今天要讲的线段树。
线段树的原理就是将区间为[1,n]分解成多个小的子区间(不超过4*n),线段树对于上述的两种操作的时间复杂度都是O(logn)的级别,相对于其他两种,时间复杂度降低了不少。
第二:线段树的图解
在这里插入图片描述上图就是对【1~13】所构建的线段树
当我们对任意一区间进行求和时:例如:【6~9】
有如图的操作:
在这里插入图片描述

在这里插入图片描述
在线段树中,我们只用找到6这个点和(8,9)这个区间,就可以求得(6,9)的和
将6+8+9的三次操作减少到了6+(8,9)两次操作,在区间范围越大的时候,就越明显。
而进行删除操作的时候,线段树的优点也体现了出来
eg:当我们将修改4这个点的值为2时

这个图片上面是区间或者,这个点所代表的值,最下面,是这个点所代表的值。
当我们修改第4点的值为2的时候
修改流程如下:
在这里插入图片描述我们可以很明确的看到,在线段树的修改中,我们只用修改一条线路即可,其时间复杂度为O(logn)
第三:代码解释
首先我们定义一个线段树:
我们先定义arr[]来储存点的值,定义tree[]来存储树的值(即区间的和)
建立线段树:

void build_date(int l,int r,int arr[],int tree,int node)
{
    
    
	if(l==r)
		tree[node]=arr[l];
	else
	{
    
    
		int mid=(l+r)>>1;//即(l+r)/2,使用>>1是因为会更快一些
		int right_node=node*2+1;
		int left_node=node*2;
		build_date(l,mid,arr,tree,left_date);
		build_date(mid+1,r,arr,tree,right_date);
		tree[node]=tree[left_node]+tree[right_node];
	}
}

在我们处理左子节点和右子节点的时候:
当根节点为0的时候:left-node=2node+1,right-node=2node+2;
当根结点为1的时候:left-node=2node ,right-node=2node+1;
在build_date中我们通过递归的方式将其tree值算出,其递归终止的条件是当(l==r),即当递归到根节点的时候,我们就跳出递归,执行tree[node]=tree[right_node]+tree[left_node];操作。向上返回,直到根节点,完成线段树的建立。

对于值的更改//指的是对于线段树中某一点的值的更改

void update_tree(int l,int r,int arr[],int tree[],int node,int idx,int val)
//即把idx处的值改为val
{
    
    
	if(l==r)
	{
    
    
		arr[idx]=val;
		tree[idx]=val;
	}
	else
	{
    
    
		int mid=(l+r)>>1;
		int left_node=node<<1;
		int right_node=node<<1|1;
		if(idx>=l&&mid>=idx)//即在左区间时
			update_tree(l,r,arr,tree,left_node,idx,val);
		else
			update_tree(l,r,arr,tree,right_node,idx,val);
		tree[node]=tree[left_node]+tree[right_node];
	}
}

对于点的更改,我们先递归至(l==r),即为叶子节点的时候就终止递归
否则,当idx>=l&&idx<=mid的时候,即当需要改变的值在当前的node的左子树的时候,向其左子树递归,否则,向其右子树递归,直到( l == r )的时候,停止递归,向前需要改变的父节点依次回归改变其值。

查询某一区间的和

int query_tree(int l,int r,int node,int arr[],int tree[],int s,int t)
//s,t分别为起点与终点
{
    
    
	if(l>s||r<t)//即完全不相容的情况
		return 0;
	else if(l==r)
		return tree[node];
	else if(l=>s&&r<=t)
		return tree[node];
	else
	{
    
    
		int mid=(l+r)>>1;
		int left_node=(l+r)<<1;
		int right_node=(l+r)<<1|1;
		int sum_left=query(l,mid,left_node,arr,tree,s,r);
		int sum_right=query(mid+1,r,right_node,arr,tree,s,t);
		return sum_left+sum_right;
	}
}

我知道有很多人都有疑惑,为什么不在对左右子树递归的时候加上if(如果只在左子树上||只在右子树上)的判断,因为在开始有:if(s<l||r<t) return 0;的判断语句,可以在某一方被完全不包括时,在第一次判断的时候就return 0;跳出,不会增大时间复杂度;

终于讲完了,最后给各位如下完整的代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int maxl=1000;//如果用99999这种数字,tree[99999]所占用的连续存储过大,电脑内存会不够,会爆掉
void build_tree(int arr[],int tree[],int node,int start,int endll)//建立一个线段树
{
    
    
    if(start==endll)
        tree[node]=arr[start];//在叶节点时,返回此点的值(在叶节点的时候,tree的值和arr的值是相等的)
    else
    {
    
    
        int mid=(start+endll)/2;
        int left_node=2*node+1;
        int right_node=2*node+2;
        build_tree(arr,tree,left_node,start,mid);//向左边递归
        build_tree(arr,tree,right_node,mid+1,endll);//向右边递归
        tree[node]=tree[left_node]+tree[right_node];//求node点的值
    }
}
void update_tree(int arr[],int tree[],int node,int start,int endll,int idx,int val)//在线段树中,进行值的更替
//idx和val的意思是:eg:arr[idx]=val,即把第idx个格子改成val
{
    
    
    if(start==endll)
    {
    
    
        arr[idx]=val;
        tree[node]=val;//在叶子处对idx处的tree和arr的值进行更新
    }
    else
    {
    
    
        int mid=(start+endll)/2;
        int left_node=2*node+1;
        int right_node=2*node+2;
        if(idx>=start&&idx<=mid)//在左区间时
            update_tree(arr,tree,left_node,start,mid,idx,val);
        else//在右区间时
            update_tree(arr,tree,right_node,mid+1,endll,idx,val);
        tree[node]=tree[left_node]+tree[right_node];//对更新的叶子节点值往上的tree的值进行更新
    }
}
int query_tree(int arr[],int tree[],int node,int start,int endll,int L,int R)//求区间的和
{
    
    
    if(R<start||L>endll)//区间完全不相容
        return 0;
    else if(start==endll)//当计算到叶节点时
        return tree[node];//返回点的值
    else if(L<=start&&endll<=R)//当一个node节点区域完全被所求的区间包括时,无需对此区域的子节点区域进行访问
        return tree[node];//直接返回该node节点的值(对时间复杂度进行优化)

    else
    {
    
    
        int mid=(start+endll)/2;
        int left_node=2*node+1;
        int right_node=2*node+2;
        int sum_left=query_tree(arr,tree,left_node,start,mid,L,R);
        int sum_right=query_tree(arr,tree,right_node,mid+1,endll,L,R);
        return sum_left+sum_right;
    }
}
int main()
{
    
    
    int arr[]={
    
    1,3,5,7,9,11};//即各个点的值
    int sizel=6;
    int tree[maxl]={
    
    0};//树上的点和的值
    build_tree(arr,tree,0,0,sizel-1);
    update_tree(arr,tree,0,0,sizel-1,4,6);
    for(int i=0;i<15;i++)
        cout<<"tree["<<i<<"]="<<tree[i]<<endl;
    int s=query_tree(arr,tree,0,0,sizel-1,2,5);
    cout<<"2~5的和为:"<<s<<endl;
    return 0;
}

猜你喜欢

转载自blog.csdn.net/malloch/article/details/107990742