线段树-经典&有趣
作者:互联网
定义
一种区间树,即将除叶子节点的区间拆分成两个区间,序列以一棵二叉树的形式呈现。
如图便是一棵线段树,我们将通过图来更深刻的认识线段树。
- 性质
1.可以观察到,这棵线段树的深度是 \(\log n\)。
2.每个除叶子节点以外节点的左孩子是该节点乘上 \(2\),而右孩子是该节点乘上 \(2\) 加上 \(1\)。
3.可以观察到此树是一棵完全二叉树。
众所周知,性质决定用途,我们可以用线段树干什么呢?
应用
- 要致富,先建树
我们令根节点为 \(1\),从根节点一直分裂,直到分到叶子节点,然后停止分裂即可。有时候需要在叶子节点传递信息,然后向上传递。
时间复杂度:\(O(n\log n)\)
代码:
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r){
t[p].sum=a[l];
return ;
}
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
pushup(p);
}
这里给出的是去见求和的建树代码。
- 然后我们需要开始最简单的单点修改&单点查询
好像可以很简单的解决呢
因为只有一个点,在还没有到达叶子节点的时候,我们看看它是属于左子树还是右子树,属于哪边就往哪边递归,递归完之后向上更新即可。
时间复杂度:\(O(\log n)\)
代码:
void change(int p,int x,int v){
if(t[p].l==t[p].r){
t[p].sum+=v;
return ;
}
int mid=(l+r)/2;
if(x<=mid) change(p*2,x,v);
else change(p*2+1,x,v);
pushup(p);
}
- 然后我们来试着区间查询
如何实现呢?
我们结合实际情况来看一下。
任然是这课树,我们假定我们查询的区间是 \([3,7]\)。
像这样,我们仍然从根节点 \(1\) 进行遍历。
我们发现根节点 \(1\) 没有完全在这个区间以内,直接返回其值肯定不对。
我们发现其左子树 \(2\) 有一部分区间在我们查询的范围内,我们递归到左子树 \(2\)。
我们发现此时到的节点其右子树 \(5\) 上有一部分在区间内,但是左子树 \(4\) 并没有,所以我们遍历到右子树。
此时我们发现区间完全涵盖,直接返回其值。
由于是递归形式,所以我们继续返回到节点 \(1\) 的右子树 \(2\)。
操作就跟原来一样就可以了。
时间复杂度:\(O(\log n)\)
代码:
int ask(int p,int l,int r){
if (t[p].l>=l && t[p].r<=r) return t[p].sum;
int mid=(t[p].l+t[p].r)/2,sum=0;
if(l<=mid) sum=ask(p*2,l,r);
if(r>mid) sum+=ask(p*2+1,l,r);
return sum;
}
- 接着我们来尝试一下区间修改
其实和区间查询类似,只不过需要引入一个叫懒标记的东西。
什么意思呢?
举一个生动形象的例子解释一下:
假设有一个像线段树一样教室:
老师现在要检查某些同学的作业。
然后前面的同学手里有答案,老师来了,他早就抄完了答案,老师来了,他就把答案送给了后面同学。
老师没来,答案就一直放在他手里。
线段树区间修改差不多就是这样,这里就不细讲了。
时间复杂度:\(O(\log n)\)
代码:
void change(int u,int l,int r,int d){
if(tr[u].l>=l && tr[u].r<=r){
tr[u].sum+=(tr[u].r-tr[u].l+1)*d;
tr[u].add+=d;
return ;
}
pushdown(u);
int mid=(tr[u].l+tr[u].r)/2;
if(l<=mid) change(u*2,l,r,d);
if(r>mid) change(u*2+1,l,r,d);
pushup(u);
}
权值线段树:
权值线段树可以解决大部分关于数值上的问题,大致就是以所有的权值开一棵线段树,在 \(\log n\) 的效率下解决大部分问题。
但是这样它的缺陷就比较明显,不适用于那些权值比较大的操作。不然在空间上劣势十分明显,但不过由于操作个数有限,我们往往可以通过离散化来进行优化。
大致操作与线段树是类似的,可以通过一些操作使得其发挥得更加淋漓尽致。
它可以解决一些平衡树问题。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int m,f[N],cnt;
map<int,int>q;
struct node{
int l,r;
int val;
}t[4*N];
struct Q{
int opt,x;
}d[N];
void pushup(int p){
t[p].val=t[p*2].val+t[p*2+1].val;
}
void build(int p,int l,int r){
t[p]=((node){l,r,0});
if(l==r) return ;
int mid=(l+r)/2;
build(p*2,l,mid);
build(p*2+1,mid+1,r);
}
void change(int p,int x,int k){
if(t[p].l==t[p].r){
t[p].val+=k;
return ;
}
int mid=(t[p].l+t[p].r)/2;
if(x<=mid) change(p*2,x,k);
else change(p*2+1,x,k);
pushup(p);
}
int Get_Rank(int p,int x){
if(t[p].l==t[p].r) return 1;
int mid=(t[p].l+t[p].r)/2;
if(x<=mid) return Get_Rank(p*2,x);
else return t[p*2].val+Get_Rank(p*2+1,x);
}
int Get_Val(int p,int x){
if(t[p].l==t[p].r) return t[p].l;
int mid=(t[p].l+t[p].r)/2;
if(t[p*2].val>=x) return Get_Val(p*2,x);
else return Get_Val(p*2+1,x-t[p*2].val);
}
int Get_Pre(int p,int x){
if(!t[p].val) return 0;
if(t[p].l==t[p].r){
if(t[p].l==x) return 0;
return t[p].l;
}
int mid=(t[p].l+t[p].r)/2;
if(mid>=x) return Get_Pre(p*2,x);
int t=Get_Pre(p*2+1,x);
if(t) return t;
return Get_Pre(p*2,x);
}
int Get_Next(int p,int x){
if(!t[p].val) return 0;
if(t[p].l==t[p].r){
if(t[p].l==x) return 0;
return t[p].l;
}
int mid=(t[p].l+t[p].r)/2;
if(mid<=x) return Get_Next(p*2+1,x);
int t=Get_Next(p*2,x);
if(t) return t;
return Get_Next(p*2+1,x);
}
int main(){
build(1,0,100000);
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%d %d",&d[i].opt,&d[i].x);
if(d[i].opt!=4) f[++cnt]=d[i].x;
}
sort(f+1,f+1+cnt);
for(int i=1;i<=cnt;i++) q[f[i]]=i;
for(int i=1;i<=m;i++){
int opt=d[i].opt,x=d[i].x;
if(opt!=4) x=q[x];
if(opt==1) change(1,x,1);
if(opt==2) change(1,x,-1);
if(opt==3) printf("%d\n",Get_Rank(1,x));
if(opt==4) printf("%d\n",f[Get_Val(1,x)]);
if(opt==5) printf("%d\n",f[Get_Pre(1,x)]);
if(opt==6) printf("%d\n",f[Get_Next(1,x)]);
}
return 0;
}
动态开点:
动态开点比较好用,它避免了一种尴尬的局面,那就是权值线段树需要进行离散化,肯定很多的。。。
咕咕咕
标签:return,val,int,线段,mid,经典,有趣,节点 来源: https://www.cnblogs.com/cqbzfsk/p/15867993.html