其他分享
首页 > 其他分享> > 线段树-经典&有趣

线段树-经典&有趣

作者:互联网

定义

一种区间树,即将除叶子节点的区间拆分成两个区间,序列以一棵二叉树的形式呈现。

如图便是一棵线段树,我们将通过图来更深刻的认识线段树。

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