今天我们以三个例题来详解并对比这几种算法的优缺点
注:若只学习概念可直接看概念及对比,例题无所谓
##线段树
线段树是一棵二叉搜索树,与区间树相似,使用它可以快速的查找一个节点在若干条线段中出现的次数,时间复杂度为 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 and−i
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;//返回
}
和线段树区间查询类似,分为两种(准确来说是三种)
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
区间查询就是,每查到一个区间,有两种选择(准确来说是三种)
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的和
洛谷P3374 树状数组1
输入有两个数 n , m n,m n,m,表示有 n n n个数, m m m次操作,有两种操作
建树,对其进行基本的单店修改和区间输出的操作
同线段树,也是比较基本的单店修改和区间输出。
详见代码
#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次操作,有两种操作
这次和上一题基本一致,不过查找和修改操作改变一下,原来的 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的值加上原本的值
}
}
洛谷P3372 线段树1
输入有两个数 n , m n,m n,m,表示有 n n n个数, m m m次操作,有两种操作
注:本人蒟蒻,只会线段树。。。
这次两个都是区间的,所以我们既要用上 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) |
应用 | 所有关于区间的题几乎都可以 | 应用不足线段树广 |
在时间复杂度上,树状数组略胜于线段树,在空间复杂度上,树状数组完爆线段树。
但是在应用上,树状数组就没有线段树用的更加广泛了(例如区间最大值),这也是线段树至今存在的理由。