ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

线段树

2022-08-15 13:30:46  阅读:157  来源: 互联网

标签:int 线段 tr build 复杂度 节点


线段树学习笔记

1. 线段树简介

线段树,是一种二叉搜索树,其每一个节点表示了一段区间。

image

线段树支持的操作有:
  • 区间求 最大/最小值,时间复杂度 \(O(logN)\)(p.s.后面代码均以求和为例)

有同学说:“这还不简单?我用前缀和 O(1) 就能做,要他干什么?”

不要着急,因为线段树还有这个功能

  • 单点修改,时间复杂度 \(\textbf{O(logN})\)

前面那位同学:“我不服,我用树状数组,也是 O(logN)!!!”

看我接着放大招,线段树还可以这样玩

  • 区间修改,时间复杂度还是 \(O(logN)\)

某同学:“……”

是的,线段树就是一个如此强大的数据结构,让我们接着一起欣赏 它的魅力

2.线段树时空复杂度分析

  • 空间复杂度 \(O(4N)\)

线段树在形态上属于平衡二叉树(此平衡树非彼平衡树 光速逃 ),也就是砍掉最后一层之后是满二叉树,就像 二叉堆 一样

由于线段树肥肠整齐,我们可以用数组来存

每个节点 u 的左右儿子分别为 u*2u*2+1

(在写代码时一般写成位运算的形式,及u<<1 u<<1|1

除最后一层外,节点总个数是 \(2n-1\),而最后一层最多是上一层的 \(2\) 倍

所以总共的节点数最多为 \(4n-1\)

因此,线段树的数组要开到 \(4N\) 这么大

这里我们可以写一个简易的程序验证一下:

#include<iostream>

using namespace std;

int a,b;

void build(int u,int l,int r)
{
	a++;
	b=max(b,u);
	if(l==r)
		return;
	int m=l+r>>1;
	build(u<<1,l,m);
	build(u<<1|1,m+1,r);
}

int main()
{
	int n;
	scanf("%d",&n);
	build(1,1,n);
	printf("length of the array: %d, number of nodes: %d, size of the space: %d;\n",n,a,b);
		
	return 0;
} 

当 \(n\) 为一比较大的个 \(2\) 的整次幂再加上一个很小的数时,占用的空间就接近 \(4n\) 了

eg:
输入:

1048580

\[1048580=2^{20}+4 \]

输出:

length of the array: 1048580, number of nodes: 2097159, size of the space: 3670017;

\(n=1048580\),却用到了 \(3670017\) 那么大的下标,绝对要开 \(4n\) 呀!

  • 时间复杂度 \(O(logN)\)

显而易见,一棵 \(n\) 个节点的二叉搜索树一共有 \(logn\) 层

最坏情况下,每次更新或查询时,都要从根节点递归到最后一层,因此时间复杂度为 \(O(logN)\)

当然,除了两条贯穿上下的链之外,还会有一些长度为 \(1\) 的分叉,因此常数较大(这可能是线段树唯一的缺点了吧,但是无伤大雅 qwq

3.线段树的存储

有两种方法
第一种就直接开一个简单的数组,但这样每个函数都需要把当前节点表示的区间的左右两个端点传成参数,太麻烦了,参数太多也不太好看
第二种方法看着就舒服多了。
需要一个结构体

struct Tree{
	int l,r;//线段所覆盖区间的左右端点
	int v;//存储的信息,即sum,max或min;
	int tag;//懒标记
}tr[4*N];

4.线段树5大函数

  • build

顾名思义,新建一颗线段树树

递归建树

void build_tree(int u,int l,int r) 
{
	if(l==r)//叶节点
	{
		tr[u]={l,r,w[l],0};
		return;
	}
	tr[u]={l,r};
	int m=l+r>>1;
	build_tree(u<<1,l,m);//递归建左子树 
	build_tree(u<<1|1,m+1,r);//右子树 
	pushup(u);
}
  • pushup

根据两个子节点信息求出父节点信息(可以解决单点修改)

void pushup(int u)
{
	tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
  • pushdown

把父节点记录的信息下发到子结点上(区间修改必备操作)

过程中需要用到懒标记

懒标记的基本原理:在反复的操作过程中,同一个节点可能被修改很多次,不如把每一次修改都先记录下来,攒到一起,最后只修改一次(非常妙的思路hh)

void pushdown(int u)
{
	if(tr[u].tag)
	{
		tr[u<<1].tag+=tr[u].tag;
		tr[u<<1|1].tag+=tr[u].tag;//下放懒标记 
		tr[u<<1].sum+=(ll)tr[u].tag*(tr[u<<1].r-tr[u<<1].l+1);
		tr[u<<1|1].sum+=(ll)tr[u].tag*(tr[u<<1|1].r-tr[u<<1|1].l+1);//更新区间和 
		tr[u].tag=0;//标记清空 
	}
}

  • update

更新/修改
将某个区间修改为某个数/加上某个数(代码以加上为例)

void update(int u,int l,int r,int k)
{
	if(tr[u].l>=l&&tr[u].r<=r)//被完全包含在内
	{
		tr[u].sum+=(ll)k*(tr[u].r-tr[u].l+1);
//		if(tr[u].r>tr[u].l)//叶节点可以不用记,因为不会再往下传导 
			tr[u].tag+=k;
		return;
	}
	pushdown(u);//更新 
	int m=tr[u].l+tr[u].r>>1;
	if(l<=m)//和左面有交集 
		update(u<<1,l,r,k);
	if(r>m)//和右面有交集 
		update(u<<1|1,l,r,k);
	pushup(u);
}

  • query

查询某个区间的和/最大值/最小值(代码以和为例)

ll query(int u,int l,int r)
{
	if(tr[u].l>=l&&tr[u].r<=r)
		return tr[u].sum;
	pushdown(u);//更新 
	ll res=0;
	int m=tr[u].l+tr[u].r>>1;
	if(l<=m)//和左面有交集 
		res+=query(u<<1,l,r);
	if(r>m)//和右面有交集 
		res+=query(u<<1|1,l,r);
	return res;//返回结果^_^ 
}

5.完整代码

这个代码可以轻松水过洛谷模板题 - P3372 【模板】线段树 1

#include<iostream>

using namespace std;

typedef long long ll;

const int N=100010;

struct Tree{
	int l,r;//区间左右端点 
	ll sum;//区间和,用不用long long 看具体情况 
	ll tag;//懒标记 ZZZ
}tr[4*N];//空间开4倍,不开见祖宗(逃~) 

int n,q;//n为数组大小,q是操作个数 
int w[N];//原数组 

void pushup(int u)
{
	tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}

void pushdown(int u)
{
	if(tr[u].tag)
	{
		tr[u<<1].tag+=tr[u].tag;
		tr[u<<1|1].tag+=tr[u].tag;//下放懒标记 
		tr[u<<1].sum+=(ll)tr[u].tag*(tr[u<<1].r-tr[u<<1].l+1);
		tr[u<<1|1].sum+=(ll)tr[u].tag*(tr[u<<1|1].r-tr[u<<1|1].l+1);//更新区间和 
		tr[u].tag=0;//标记清空 
	}
}

void build_tree(int u,int l,int r) 
{
	if(l==r)//叶节点
	{
		tr[u]={l,r,w[l],0};
		return;
	}
	tr[u]={l,r};
	int m=l+r>>1;
	build_tree(u<<1,l,m);//递归建左子树 
	build_tree(u<<1|1,m+1,r);//右子树 
	pushup(u);
}

void update(int u,int l,int r,int k)
{
	if(tr[u].l>=l&&tr[u].r<=r)//被完全包含在内
	{
		tr[u].sum+=(ll)k*(tr[u].r-tr[u].l+1);
//		if(tr[u].r>tr[u].l)//叶节点可以不用记,因为不会再往下传导 
			tr[u].tag+=k;
		return;
	}
	pushdown(u);//更新 
	int m=tr[u].l+tr[u].r>>1;
	if(l<=m)//和左面有交集 
		update(u<<1,l,r,k);
	if(r>m)//和右面有交集 
		update(u<<1|1,l,r,k);
	pushup(u);
}

ll query(int u,int l,int r)
{
	if(tr[u].l>=l&&tr[u].r<=r)
		return tr[u].sum;
	pushdown(u);//更新 
	ll res=0;
	int m=tr[u].l+tr[u].r>>1;
	if(l<=m)//和左面有交集 
		res+=query(u<<1,l,r);
	if(r>m)//和右面有交集 
		res+=query(u<<1|1,l,r);
	return res;//返回结果^_^ 
}

int main()
{
	cin>>n>>q;
	for(int i=1;i<=n;i++)
		cin>>w[i];//输入原数组 
	build_tree(1,1,n);//建立线段树 
	while(q--)
	{
		int opt,l,r,k;
		cin>>opt;//操作种类 
		if(opt==1)
		{
			cin>>l>>r>>k;
			update(1,l,r,k);//更新 
		}else{
			cin>>l>>r;
			cout<<query(1,l,r)<<endl;//查询 
		}
	}
	
	return 0;
} 

恭喜你!线段树基本知识已经掌握辣~乌拉~~

小题一道试试水(简单的模板题):- P1253 [yLOI2018] 扶苏的问题

本文章到此结束,喜欢的童鞋点个赞呗 ~ 笔芯

追风赶月莫停留,平芜尽处是春山~

标签:int,线段,tr,build,复杂度,节点
来源: https://www.cnblogs.com/Orange-Star/p/16584075.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有