线段树详解(附例题)

线段树

线段树是一种基于分治思想的二叉树,它的每个节点对应一个区间[L,R],叶子节点的区间L=R。非叶子节点[L,R]的左孩子区间为[L,(L+R)/2],右孩子区间为[(L+R)/2+1,R]。 [1,10]区间的线段树:

alt

「1、线段树的存储方式」
对于区间最值(最大值或最小值)查询问题,线段树的每个节点包含三个域:l、r、mx,其中l和r表示区间的左右端点,mx表示[l, r]区间最值。 线段树除了最后一层,其他层构成一颗满二叉树,因此采用顺序存储方式,用一个数组tree[]存储节点。

alt

以最大值为例,10个元素a[1..10]={5,3,7,2,12,1,6,4,8,15},线段树如下。

alt

线段树需要开辟4n的空间

线段树的叶子结点有n个,那么这n个叶子结点最坏情况会在不同的两层上,即倒数2层,设线段树共有k层,以第k层只有2个叶子结点为例。如图所示,则第k-1层有n-2个叶子结点和1个非叶子结点,共n-1个结点,则第k层应有2(n-1)个空间结点,由满二叉树性质可知,前k-2层应有n-2个结点空间,所以总空间为 个。忽略少数空间,所以开辟空间时需要4n个空间。

alt

「2、创建线段树」
可以采用递归的方法创建线段树。 算法步骤:

  • 若是叶子节点(l=r),则节点的最值就是对应位置的元素值。
  • 若是非叶子节点,则递归创建左子树和右子树。
  • 节点的区间最值等于该节点左右子树最值的最大值。

「代码」

#define lc k<<1  //左孩子下标 2*k 
#define rc k<<1|1 //右孩子下标 2*k+1
const int maxn=10005;
const int inf=0x7fffffff;
int n,a[maxn];

struct node{
 int l,r,mx;//l,r表示区间的左右端点,mx表示区间[l,r]的最大值 
}tree[maxn*4];


//构建线段树 
void build(int k,int l,int r)//k:线段树数组下标,l,r分别为结点的左右区间 
 tree[k].l=l;
 tree[k].r=r;
 if(l==r){
  tree[k].mx=a[l];
  return;
 }
 int mid=l+(r-l>>1);
 build(lc,l,mid);
 build(rc,mid+1,r);
 tree[k].mx=max(tree[lc].mx,tree[rc].mx);
}

「3、点更新」
点更新指修改一个元素的值,例如将a[i]修改为v。 算法步骤: (1)若是叶子节点,满足l=r且l=i,则修改该节点的最值为v。 (2)若是非叶子节点,则判断是在左子树中更新还是在右子树中更新。 (3)返回时更新节点的最值。

「代码」

#define lc k<<1  //左孩子下标 2*k 
#define rc k<<1|1 //右孩子下标 2*k+1
const int maxn=10005;
const int inf=0x7fffffff;
int n,a[maxn];

struct node{
 int l,r,mx;//l,r表示区间的左右端点,mx表示区间[l,r]的最大值 
}tree[maxn*4];

//更新线段树
void update(int k,int i,int v)//k:线段树数组下标,i:更新的数组下标,v:更新后的值 
 if(tree[k].l==tree[k].r&&tree[k].l==i){
  tree[k].mx=v; 
  return;
 }
 int mid=tree[k].l+(tree[k].r-tree[k].l>>1);
 if(i<=mid) update(lc,i,v);
 else update(rc,i,v);
 tree[k].mx=max(tree[lc].mx,tree[rc].mx);
}

例如,修改第5个节点的值为14,先从树根向下找第5个元素所在的叶子节点,将其最值修改为14,返回时更新路径上所有节点的最值(左右子节点最值的最大值)。

alt

「4.区间查询」
区间查询指查询一个[l,r]区间的最值。
算法步骤:
(1)若节点所在的区间被查询区间[1,r]覆盖,则返回该节点的最值。

(2)判断是在左子树中查询,还是在右子树中查询。

(3)返回最值。

「代码」

#define lc k<<1  //左孩子下标 2*k 
#define rc k<<1|1 //右孩子下标 2*k+1
const int maxn=10005;
const int inf=0x7fffffff;
int n,a[maxn];

struct node{
 int l,r,mx;//l,r表示区间的左右端点,mx表示区间[l,r]的最大值 
}tree[maxn*4];

//查询区间最值(区间覆盖)
int query1(int k,int l,int r)//k:线段树数组下标,l,r为查询区间范围 
 if(tree[k].l>=l&&tree[k].r<=r) return tree[k].mx;
 int mid=tree[k].l+(tree[k].r-tree[k].l>>1);
 int Max=-inf;
 if(l<=mid) Max=max(Max,query1(lc,l,r));
 if(r>mid) Max=max(Max,query1(rc,l,r));
 return Max;


//查询区间最值(区间分割)
int query2(int k,int l,int r)//k:线段树数组下标,l,r为查询区间范围 
 if(tree[k].l==l&&tree[k].r==r) return tree[k].mx;
 int mid=tree[k].l+(tree[k].r-tree[k].l>>1);
 int Max=-inf;
 if(l<=mid) Max=max(Max,query2(lc,l,mid));
 if(r>mid) Max=max(Max,query2(rc,mid+1,r));
 return Max;

「算法分析:」
树上的操作大多与树高相关。因为线段树对区间进行二分,是一棵平衡二叉树,树高为O(logn),查询和更新的时间复杂度均为O(logn),空间复杂度为O(n)。线段树主要用于更新和查询,一般至少有一个是区间更新或查询。更新和查询的种类变化多样,灵活运用线段树可以解决多种问题。

「拓展:懒标记(难点)」

假设有数组a,现有2种操作,一种是将数组中区间[l,r]的值改为k,另一种是查询区间[l,r]的和,并且这2中操作次数足够多,我们应该怎么解决呢?

如果用原有线段树,那么每次都要把区间[l,r]所有值都修改,如果再进行一次修改,那么上一次修改可能就无济于事了。

在看一种情况,首先进行修改区间[x,y]的值为d,再查询区间[x,y]的和。聪明的你一定能想到,直接用(y-x+1)*d就好了,对!那这样做的话,是不是只要修改这个区间值为d,然后用 就好了。那你可能会问,这是巧了,如果我查询的区间和修改的区间不同呢?你是不是就要把每个区间值都修改呢?不!我们还是不用全部修改,只需要修改我们用到的区间即可。为了便于理解我们以下面例子展开介绍。

现有数组a[]={1,2,3,4,5,6,7,8,9,10},(下标从1开始)先要进行4次操作(设修改的值大于0),分别是:

(1)将区间[1,5]的值改为3。

(2)求区间[1,5]的和。

(3)将区间[1,3]的值改为4。

(4)查询区间[3,4]的值。

「解析」

alt
alt
alt
alt
alt
alt
alt
alt

<<< 左右滑动见更多 >>>

上述演示的是下发(pushdown)的案例,于此同时也有回归(pushup)。回归就类似于递归,当子结点发生改变,同时子节点的改变会影响父节点的值,因此要回归。

举个例子,记录区间[1,2]最大值max=3,当其子区间[1,1]值改为5时,则父区间[1,2]的最大值也因发生改变,因此需要回归改变区间[1,2]的最大值为5。

「例题」

Problem - 1166 (hdu.edu.cn)

3468 -- A Simple Problem with Integers (poj.org)

Problem - 4902 (hdu.edu.cn)

2777 -- Count Color (poj.org)

本文由 mdnice 多平台发布

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白沐沐vccc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值