Calmsym学习数据结构与算法之 线段树


前言

在之前的学习中,关于数组维护,我们已经学习了前缀和,差分以及树状数组,对于不同的体型我们可以运用不同的数据结构来进行维护,今天我们来讲一个更为通用的数据结构–线段树

线段树

1.1 什么是线段树

线段树(Segment Tree) 是一种基于分治思想的二叉树结构(问什么是二叉树请百度),相比于树状数组里运用二进制分解来储存区间,线段树是将一个线性结构(比如数组)表示为一棵树,每个节点表示一个区间,叶节点来表示单个元素,线段树更易理解也更为通用,被广泛使用于算法竞赛中,唯一的缺点可能是他的代码量相当大…

1.2 原理与操作

线段树的主要思想是将原始数据分成若干区间,并用这些区间去构建一颗二叉树,每个节点代表一个区间,包块左右边界和区间内的信息(比如最大值,最小值,区间和等等)
线段树通常用于解决区间查询问题,如查询区间最值,区间和,他还支持区间更新操作,比如更新区间所有元素。
线段树的构建和查询操作的时间复杂度均为O(logN),其中N为原始数组的长度,因此线段树是一种高效的数据结构,对于需要频繁操作的题目有着相当高的优先级。
线段树支持以下操作:

	1.单点修改
	2.单点查询
	3.区间修改
	4.区间查询

1.3 线段树的性质

在讲操作实现我们先来看看线段树的构成具有什么性质,来帮助我们更好地理解各个操作的实现
1.线段树每个节点都代表一个区间
2.线段树具有唯一的根节点,代表的区间是整个统计范围, 如 [ 1 , N ]
3.线段树的每个叶节点都代表一个长度为 1 的 “元区间” 如 [ x , x ]
4.对于每个内部节点 [ l , r ] ,它的左子节点是 [ l , mid ] , 右子节点是 [ mid+1 , r ] 其中 mid = ( l + r )/ 2(向下取整)

所以对于一棵线段树,除去树的最后一层,整颗线段树一定是一棵完全二叉树,树的深度为O(logN),因此我们可以按照与二叉堆类似的方式编写节点;

1.根节点编号为1
2.编号为x的节点的左子节点编号为x2 ,右子节点编号为 x2+1

操作

2.1 线段树的创建 build

struct SegmentTree{
     int l,r,w;
}tree[N*4];

int a[N];

void build(int p,int left,int right){
     tree[p].l=left;
     tree[p].r=right;
     if(left==right){
          tree[p].w=a[left];
          return ;
     }
     int mid=(left+right)/2;
     build(p*2 , left , mid);
     build(p*2+1 , mid+1 , right);
     tree[p].w=max(tree[p*2].w , tree[p*2+1].w);
     //记录最值,从下往上传递;
     //举一反三,我们同样可以在结构体中定义如.tot 用于记录区间和
}

2.2 线段树的单点修改

单点修改是一条形如 “ c x v ” 的指令,表示把 a[ x ] 的值修改为 v

在线段树中,根节点是执行各种指令的入口。我们需要从根节点出发,递归找到区间 [ x , x ] 的叶节点,然后从下往上更新它以及所有包含它的父节点,时间复杂度为O(logN)

void change(int p,int x,int v){
     if(tree[p].l == tree[p].r){
          tree[p].w= v;
          return ;
     }
     int mid = (tree[p].l + tree[p].r)/2;
     if(x < mid) change(p*2,x,v);
     else change(P*2+1,x,v);
     tree[p].w = max(tree[p*2].w , tree[p*2+1].w);
     //从下向上更新
}

2.3 线段树的区间查询

区间查询是一条形如“ Q l r ”的指令,例如查询序列 a 在区间 [ l , r ] 上的最大值,我们只需要递归执行以下过程
1.若 [ l , r ] 完全覆盖了当前节点代表的区间,即立即回溯(因为我们已经记录了当前区间的最值),并且该节点的w值作为候选答案

2.若左子节点与 [ l , r ] 有重叠部分,则递归访问左子节点

3.若右子节点与 [ l , r ] 有重叠部分,则递归访问右子节点

int ask(int p,int left,int right){
     if(left <= tree[p].l && right >= tree[p].r) return tree[p].w;
     //完全包含的情况
     int mid = (tree[p].l + tree[p].r) / 2;
     int val = -INF;//负无穷大
     if(left <= mid) val = max(val,ask(p*2,left,right));
     if(right > mid) val = max(val,ask(2*p+1,left,right));
     return val;
}

2.4 lazytag 懒标记–延迟标记

在线段树的“区间查询”中,根据上面的证明和操作,我们知道能在至多 O(logN)下完成答案查询。但是在区间修改下,当某节点被修改区间 [ l , r ] 完全覆盖时,以这个节点为根的整棵子树的所有节点都会变化,若我们每次都对其进行更新,那么时间复杂度就会上升为O(N)大大降低效率。

我们知道,更新节点是为了查询而服务的,我们设要修改的区间为[ p1 , p2 ] ,其被区间 [ l , r ] 完全覆盖,当我们查询区间 [ l , r ] 时,有时无需用到其子区间,那么我们在修改区间 [ l , r ] ,根本无需更新子区间[ p1 , p2 ] 。
但在需要时又该怎么办,这里我们引入一个“懒标记”(或延迟标记)的概念。

懒标记是线段树的精髓所在。对于区间修改,朴素的想法是用递归的方式一层层修改(类似于线段树的建立),但这样的时间复杂度比较高。使用懒标记后,对于那些正好是线段树节点的区间,我们不继续递归下去,而是打上一个标记,将来要用到它的子区间的时候,再向下传递。

也就是说,除了在修改指令中直接划分的O(logN)个节点之外,对任意节点的修改都延迟到 “后续操作中递归进入其父节点时” 再执行更新操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值