线段树&树状数组

前言

今天我们以三个例题来详解并对比这几种算法的优缺点
注:若只学习概念可直接看概念及对比,例题无所谓

概念

##线段树
线段树是一棵二叉搜索树,与区间树相似,使用它可以快速的查找一个节点在若干条线段中出现的次数,时间复杂度为 O ( l o g n ) O(logn) O(logn)
##树状数组
树状数组就比较奇特了,它的节点会比线段树少一点,例如:
c1=a1;
c2=a1+a2;
c3=a3;
c4=a1+a2+a3+a4;
以此类推,这样子好像看不出什么,但把它们转换成二进制:
c0001=a0001
c0010=a0001+a0010
c0011=a0011
c0100=a0001+a0010+a0011+a0100
将每一个二进制,去掉所有高位1,只留下最低位的1,然后从那个数一直加到1,去掉前面所有的高位的1有一个很骚的东西叫做 l w o b i t lwobit lwobit,它公式的原理我也不清楚总之就是
l o w b i t ( i ) = i   a n d − i lowbit(i)=i\ and-i lowbit(i)=i andi
C++里面是
l o w b i t ( i ) = i & − i lowbit(i)=i\&-i lowbit(i)=i&i
是不是很神奇。。。

建树

线段树

就不断递归左右孩子直到递归到 l = r l=r l=r,表示为叶子节点时结束,具体看需要去建树

void build(int k,int a,int b)//k表示当前点编号,a表示左边界,b表示右边界
{
    tree[k].l=a;
    tree[k].r=b;//赋值
    if(a==b) return;//到达叶子节点
    build(lson,a,mid);build(rson,mid+1,b);//递归下去,lson表示左二子,公式为2*i(i<>1
}

树状数组

或许没有

单点修改

线段树

直接找到这个点修改,注意修改了之后要考虑它的儿子是否要修改

void updata(int k,int x,int y)//k为当前点编号,x为要加的点的编号,y为加的数
{
    tree[k].num+=y;//直接加
    if(tree[k].l==tree[k].r) return;//到叶子节点就结束
    if(tree[lson].r>=x) updata(lson,x,y);//左区间可以加
    if(tree[rson].l<=x) updata(rson,x,y);//右区间可以加
}

树状数组

如果改变 x x x的值,就要加上自己的 l o w b i t lowbit lowbit,一直加到 n n n,这些节点都要加

void add(int x,int k)
{
    for(;x<=n;x+=lb(x)) tree[x]+=k;//一直加到n,lowbit一直跟着,tree一直加
}

单点查询

线段树

就是从根节点,一直搜索到目标节点,然后一路上都加上就好了

void find(int k,int x)
{
    ans+=tree[k].num;//一直加过去
    if(tree[k].l==tree[k].r) return;//到达叶子节点就退出
    if(tree[lson].r>=x) find(lson,x);//左边有
    if(tree[rson].l<=x) find(rson,x);//右边有
}

树状数组

x x x点,一直 − l o w b i t ( x ) -lowbit(x) lowbit(x),沿途都加上就好啦

int sum(int x)
{
    int ans=0;//清0
    for(;x!=0;x-=lb(x)) ans+=tree[x];//加过去
    return ans;//返回
}

区间修改

线段树

和线段树区间查询类似,分为两种(准确来说是三种)

  1. 如果当前区间完全属于要加的区间,那么这个区间,也就是节点加上,然后return;
  2. 哪边区间有就搜哪边
void updata(int k,int a,int b,int x)
{
    if(tree[k].l>=a&&tree[k].r<=b)//完全包含
    {
    	tree[k].num+=x;//直接加
    	return;
    }
    if(tree[lson].r>=a) updata(lson,a,b,x);//左边有
    if(tree[rson].l<=b) updata(rson,a,b,x);//右边有
}

树状数组

如果将 x x x y y y区间加上一个 k k k,那就是从 x x x n n n都加上一个k,再从 y + 1 y+1 y+1 n n n加上一个 − k -k k

区间查询

线段树

区间查询就是,每查到一个区间,有两种选择(准确来说是三种)

  1. 如果这个区间被完全包括在目标区间内,那么加上这个区间的和,然后return;
  2. 哪边有就查哪边
void find(int k,int a,int b)
{
    if(tree[k].l>=a&&tree[k].r<=b)//完全包含
    {
        ans+=tree[k].num;//直接加
        return;
    }
    if(tree[lson].r>=a) find(lson,a,b);//找左边
    if(tree[rson].l<=b) find(rson,a,b);//找右边
}

树状数组

就是前缀和,比如查询 x x x y y y区间的和,那么就将从1到y的和减去1到 x x x的和

例题

1

洛谷P3374 树状数组1

大意

输入有两个数 n , m n,m n,m,表示有 n n n个数, m m m次操作,有两种操作

  1. 将第x个数加上k
  2. 输出区间[x,y]内每个数的和

思路

#线段树

建树,对其进行基本的单店修改和区间输出的操作

#树状数组

同线段树,也是比较基本的单店修改和区间输出。

详见代码

代码

#线段树

#include
#define lson (k<<1)
#define rson (k<<1|1)
#define mid ((a+b)>>1)//基本常量
using namespace std;
struct node
{
    int l,r,num;//l表示左,r表示右,num为这区间的和
}tree[2000001];
int n,c[500001],ans,m,x,y,z;//变量
void build(int k,int a,int b)//建立左右
{
    tree[k].l=a;
    tree[k].r=b;//赋值
    if(a==b) return;//到达叶子节点
    build(lson,a,mid);build(rson,mid+1,b);//继续建
}
int add(int k)//也是建树,不过这次是建num
{
    if(tree[k].l==tree[k].r) return tree[k].num=c[tree[k].r];//到达叶子节点
    return tree[k].num=add(lson)+add(rson);//继续建
}
void updata(int k,int x,int y)//单点修改,将x点加上y
{
    tree[k].num+=y;//直接加上
    if(tree[k].l==tree[k].r) return;//到达叶子节点就结束
    if(tree[lson].r>=x) updata(lson,x,y);//给左儿子加
    if(tree[rson].l<=x) updata(rson,x,y);//给右儿子加
}
void find(int k,int a,int b)//查询a到b之间的和
{
    if(tree[k].l>=a&&tree[k].r<=b)//完全包含
    {
        ans+=tree[k].num;//直接加上
        return;//然后返回
    }
    if(tree[lson].r>=a) find(lson,a,b);//左区间有
    if(tree[rson].l<=b) find(rson,a,b);//右区间有
}
int main()
{
    scanf("%d%d",&n,&m);//输入
    for(int i=1;i<=n;i++) scanf("%d",&c[i]);//输入
    build(1,1,n);//建造l,r
    add(1);//建造num
    while(m--)
    {
        scanf("%d%d%d",&x,&y,&z);
        if(x==1)
         updata(1,y,z);//修改
        else
        {
            ans=0;
            find(1,y,z);//查询
            printf("%d\n",ans);//输出
        }
    }
}

#树状数组

#include
#define lb(k) k&-k//lowbit
using namespace std;
int n,m,tree[500001],k,x,y,z;
void add(int x,int k)//单店修改
{
    for(;x<=n;x+=lb(x)) tree[x]+=k;//一直加过去
}
int sum(int x)//去1-x的和
{
    int ans=0;
    for(;x!=0;x-=lb(x)) ans+=tree[x];//加过去
    return ans;//返回
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&k);//输入
        add(i,k);//增加
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        if(x==1) add(y,z);//给y点增加z
        else printf("%d\n",sum(z)-sum(y-1));//y到z的和等于1到z的和减去1到y-1的和(前缀和定理)
    }
}

##2
洛谷P3368 树状数组2

大意

输入有两个数 n , m n,m n,m,表示有 n n n个数, m m m次操作,有两种操作

  1. 将区间[x,y]内每个数加上k
  2. 输出第x个数的值

思路

线段树

这次和上一题基本一致,不过查找和修改操作改变一下,原来的 n u m num num不再表示这区间的和,而是表示这区间加上的值(相当于 l a z y lazy lazy

树状数组

思想和线段树一致,都是将原来的区间和改为 l a z y lazy lazy的意义

代码

线段树

#include
#define lson (k<<1)
#define rson (k<<1|1)
#define mid ((a+b)>>1)
using namespace std;
struct node
{
    int l,r,num;//此处的num表示的东西改变
}tree[2000001];
int n,c[500001],ans,m,x,y,z,k;
void build(int k,int a,int b)
{
    tree[k].l=a;
    tree[k].r=b;
    if(a==b) return;
    build(lson,a,mid);build(rson,mid+1,b);//建树和上一题一致
}
void updata(int k,int a,int b,int x)//区间修改
{
    if(tree[k].l>=a&&tree[k].r<=b)//完全包含直接加
    {
    	tree[k].num+=x;
    	return;//返回
    }
    if(tree[lson].r>=a) updata(lson,a,b,x);//查询左
    if(tree[rson].l<=b) updata(rson,a,b,x);//查询右
}
void find(int k,int x)//查找此点的值
{
    ans+=tree[k].num;//一路加上lazy
    if(tree[k].l==tree[k].r) return;
    if(tree[lson].r>=x) find(lson,x);//查找左边
    if(tree[rson].l<=x) find(rson,x);//查找右边
}
int main()
{
    scanf("%d%d",&n,&m);//输入
    for(int i=1;i<=n;i++) scanf("%d",&c[i]);//输入
    build(1,1,n);//建l,r,注意这次没有建num
    while(m--)
    {
        scanf("%d",&k);//输入
        if(k==1)
        {
        	scanf("%d%d%d",&x,&y,&z);
        	updata(1,x,y,z);//区间修改
        }
        else
        {
        	ans=0;
        	scanf("%d",&x);
        	find(1,x);
        	printf("%d\n",ans+c[x]);//lazy的值加上原本的值即为此答案
        }
    }
}

树状数组

#include
#define lb(k) k&-k
using namespace std;
int n,m,tree[500001],k,x,y,z,a[500001];
void add(int x,int k)
{
    for(;x<=n;x+=lb(x)) tree[x]+=k;//区间加上lazy
}
int sum(int x)
{
    int ans=0;
    for(;x!=0;x-=lb(x)) ans+=tree[x];//求区间lazy
    return ans;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);//输入,注意这里也没有加进去
    for(int i=1;i<=m;i++)
    {
        scanf("%d",&k);
        if(k==1) 
        {scanf("%d%d%d",&x,&y,&z);add(x,z);add(y+1,-z);}//lazy操作
        else {scanf("%d",&x);printf("%d\n",sum(x)+a[x]);}//答案即为lazy的值加上原本的值
    }
}

3

洛谷P3372 线段树1

大意

输入有两个数 n , m n,m n,m,表示有 n n n个数, m m m次操作,有两种操作

  1. 将区间[x,y]内每个数加上k
  2. 输出区间[x,y]内每个数的和

思路

注:本人蒟蒻,只会线段树。。。
这次两个都是区间的,所以我们既要用上 n u m num num,又要用上 l a z y lazy lazy

代码

#include
#define lson (k<<1)
#define rson (k<<1|1)
#define mid ((a+b)>>1)
#define LL long long
using namespace std;
struct node
{
    LL l,r,num,lazy;//注意这里又有lazy又有num
}tree[400001];
LL n,c[100001],ans,m,x,y,z,k,L=1;//变量
LL slazy(LL k){return (tree[k].r-tree[k].l+1)*tree[k].lazy;}//求出这个点lazy的值
void build(LL k,LL a,LL b)//建l,r
{
    tree[k].l=a;
    tree[k].r=b;
    if(a==b) return;
    build(lson,a,mid);build(rson,mid+1,b);
}
LL add(LL k)//建立num
{
    if(tree[k].l==tree[k].r) return tree[k].num=c[tree[k].r];
    return tree[k].num=add(lson)+add(rson);
}
void push(LL k)//下传命令
{
	if(!tree[k].lazy) return;//没有的话停止下传
	tree[lson].lazy+=tree[k].lazy;//下传左儿子
	tree[rson].lazy+=tree[k].lazy;//下传右儿子
	tree[k].num+=slazy(k);//加上
	tree[k].lazy=0;//清0
}
void updata(LL k,LL a,LL b,LL x)//区间修改
{
	if(tree[k].l==a&&tree[k].r==b)//若正好相等
	{
		tree[k].lazy+=x;//直接加上lazy
		return;//退出
	}
	push(k);//下传
	if(tree[lson].r>=b) updata(lson,a,b,x);else//左边有
	if(tree[rson].l<=a) updata(rson,a,b,x);else//右边有
	{
		updata(lson,a,tree[lson].r,x);
		updata(rson,tree[rson].l,b,x);//两边都有
	}
	tree[k].num=tree[lson].num+tree[rson].num+slazy(lson)+slazy(rson);//加上
}
LL find(LL k,LL a,LL b)//区间和
{
	if(tree[k].l==a&&tree[k].r==b)
	 return tree[k].num+slazy(k);//正好相等直接计算
	push(k);//下传
	if(tree[lson].r>=b) return find(lson,a,b);else//左边有
	if(tree[rson].l<=a) return find(rson,a,b);else//右边有
	return find(lson,a,tree[lson].r)+find(rson,tree[rson].l,b);//两边皆有
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>c[i];//输入
    build(1,1,n);//建l,r
    add(1);//建num
    while(m--)
    {
    	cin>>k;
        if(k==1)
        {
        	cin>>x>>y>>z;//输入
        	updata(1,x,y,z);//修改
        }
        else
        {
        	cin>>x>>y;//输入
        	cout<<find(1,x,y)<<endl;//查找
        }
    }
}

对比

属性 线段树 树状数组
时间复杂度 n l o g n nlogn nlogn 建造: n l o g n nlogn nlogn,查询: l o g n logn logn(最坏情况)
空间复杂度 O ( 4 n ) O(4n) O(4n)(在没用离散前) O ( n ) O(n) O(n)
应用 所有关于区间的题几乎都可以 应用不足线段树广

在时间复杂度上,树状数组略胜于线段树,在空间复杂度上,树状数组完爆线段树。
但是在应用上,树状数组就没有线段树用的更加广泛了(例如区间最大值),这也是线段树至今存在的理由。

你可能感兴趣的:(库,算法讲解,树)