超详解线段树(浅显易懂)

一,什么是线段树?

    • 线段树是怎样的树形结构?

  线段树是一种二叉搜索树,而二叉搜索树,首先满足二叉树,即每个结点最多有两颗子树,并且是一颗搜索树,我们要知道,线段树的每个结点都存储了一个区间,也可以理解成一个线段,而搜索,就是在这些线段上进行搜索操作得到你想要的答案

    • 线段树能够解决什么样的问题?

  线段树的适用范围很广,可以在线维护修改以及查询区间上的最值求和。对于线段树来说,每次更新以及查询的时间复杂度为O(logN)

    • 线段树和其他RMQ算法的区别

  常用的解决RMQ问题ST算法,二者预处理时间都是O(NlogN)

(详见ST算法解决BMQ问题详解),而且ST算法的单次查询操作是O(1),看起来比线段树好多了,但二者的区别在于线段树支持在线更新值,而ST算法不支持在线操作

1. 静态的区间询问:ST表

2. 动态的区间询问:线段树/树状数组

静态指的是,数字 不会发生 改变
动态指的是,在 查询过程中,数字可能会发生一定程度的 修改

二,线段树的基本操作

建树

思路

首先,我们得先明白几件事情:

 每个结点存什么

结点下标是什么

如何建树

下面我以一个简单的求一段区间的最大值来描述上面的三个概念。

超详解线段树(浅显易懂)_第1张图片

  对于a[1~6] = {1,8,6,4,3,5}来说,线段树如上所示,红色代表每个结点存储的区间蓝色代表该区间最值

  可以发现,每个叶子结点的值就是数组的值,每个非叶子结点的度都为二,且左右两个孩子分别存储父亲一半的区间。每个父亲的存储的值也就是两个孩子存储的值的最大值。

  那么结点到底是如何存储区间的呢,以及如何快速找到非叶子结点的孩子以及非根节点的父亲呢,这里也就是理解线段树的重点以及难点所在,线段树你只要理解了结点与结点之间的关系便能很快理解线段树的基本知识。

  对于一个区间[l,r]来说,最重要的数据当然就是区间的左右端点l和r,但是大部分的情况我们并不会存储两个数值,而是通过递归传参方式进行传递。这种方式可以直接用数组存树,那这里怎么快速使用下标找到左右子树呢?

  对于上述线段树,我们增加绿色数字为每个结点的下标

超详解线段树(浅显易懂)_第2张图片

  则每个结点下标如上所示,这里你可能会问,为什么最下一排的下标直接从9跳到了12,因为中间其实是有两个空间的呀!!

超详解线段树(浅显易懂)_第3张图片

虽然没有使用,但是他已经开了两个空间,这也是为什么线段树建树需要2*2k(2k-1 < n < 2k)空间,一般会开到4*n的空间防止RE。

  仔细观察每个父节点和子节点下标的关系,不难发现以下规律

l = fa*2 (左子树下标为父节点下标的两倍)
r = fa*2+1(右子树下标为父节点下标的两倍+1)

那么明白了数组如何存线段树,结点间的关系,

建树时每次递归就要先判断l是否等于r,等于就说明是叶子节点,也就是区间是[l,l],直接赋值a[l]/a[r],再返回

否则就递归构造左儿子结点和递归构造右儿子结点,最后更新父节点

是不是觉得其实很简单。

详细代码

void bui(int id,int l,int r)//创建线段树,id表示存储下标,区间[L,r]
{
  if(l == r)//左端点等于右端点,即为叶子节点(区间长度为1),直接赋值即可
  {
    tr[id] = a[l];
    return ;
  }
// 否则将当前区间中间拆开成两个区间
  int mid = (l + r) / 2;//mid则为中间点,左儿子的结点区间为[l,mid],右儿子的结点区间为[mid + 1,r]
  bui(id * 2,l,mid); //递归构造左儿子结点
  bui(id * 2 + 1,mid + 1,r); //递归构造右儿子结点
// 左右两个区间计算完成以后
// 合并到当前区间
  tr[id] = min(tr[id * 2],tr[id * 2 + 1]);//更新父节点
}

看完代码是不是很清晰,这里也建议自己再次手动实现一遍理解递归的思路。

区间查询

思路

我们知道线段树的每个结点存储的都是一段区间的信息 ,如果我们刚好要查询这个区间,那么则直接返回这个结点的信息即可,比如对于上面线段树,如果我直接查询[1,6]这个区间的最值,那么直接返回根节点信息返回13即可,但是一般我们不会凑巧刚好查询那些区间,比如现在我要查询[2,5]区间的最值,这时候该怎么办呢,我们来看看哪些区间被[2,5]包含了。

超详解线段树(浅显易懂)_第4张图片

  一共有5个区间,而且我们可以发现[4,5]这个区间已经包含了两个子树的信息([4,4],[5,5]),所以我们需要查询的区间只有三个,分别是[2,2],[3,3],[4,5],到这里你能通过更新的思路想出来查询的思路吗? 我们还是从根节点开始往下递归,如果当前结点是被要查询的区间包含了的,则返回这个结点的信息,这样从根节点往下递归,时间复杂度也是O(logN)

详细代码

//id 表示树节点编号,l r 表示这个节点所对应的区间
//x y表示查询的区间
int find(int id,int l,int r,int x,int y)
{
  //需要查询的区间[x,y]将当前区间[l,r]包含的时候
  if(x <= l && r <= y) return tr[id];
  int mid = (l + r) / 2,ans = -INT_MAX;
   // 如果需要查询左半区间
  if(x <= mid) ans = min(ans,find(id * 2,l,mid,x,y));   
  // 如果需要查询右半区间
  if(y > mid) ans = min(ans,find(id * 2 + 1,mid + 1,r,x,y));
  return ans;
}

  如果你能理解建树的过程,那么这里的区间查询也不会太难理解。还是建议再次手动实现。

单点更新

思路

很简单,就是从根节点递归去找a[x],找到了就返回,并再返回的一路上断更新其父节点的max值。

详细代码

// id 表示树节点编号,l r 表示这个节点所对应的区间
// 将 a[x] 修改为 v
// 线段树单点更新
void gexi(int id, int l, int r, int x, int v)
{
// 找到长度为 1 的区间才返回
  if (l == r)
  {
    tr[id] = v;
    return;
  }
//否则找到 x 在左区间或者右区间去更新
  int mid = (l + r) / 2;
  if (x <= mid) gexi(id * 2, l, mid, x, v);// 需要修改的值在左区间
  else gexi(id * 2 + 1, mid + 1, r, x, v);// 需要修改的值在右区间
  tr[id] = max(tr[id * 2], tr[id * 2 + 1]);
}

三,例题

题目

超详解线段树(浅显易懂)_第5张图片

思路

输入a数组,再创建线段树,最后每次输入时判断p是否为1,是则输出find(1,1,m,x,y);否则gexi(1,x,1,m,y)。

代码

#include 
using namespace std;
int tr[10000001];
int a[10000001];
void bui(int id,int l,int r)//创建线段树,id表示存储下标,区间[L,r]
{
  if(l == r)
  {
    //叶子
    tr[id] = a[l];
    return ;
  }
  int mid = (l + r) / 2;
  bui(id * 2,l,mid);
  bui(id * 2 + 1,mid + 1,r);
  tr[id] = min(tr[id * 2],tr[id * 2 + 1]);
}
//id 表示树节点编号,l r 表示这个节点所对应的区间
//x y表示查询的区间
int find(int id,int l,int r,int x,int y)
{
  //需要查询的区间[x,y]将当前区间[l,r]包含的时候
  if(x <= l && r <= y) return tr[id];
  int mid = (l + r) / 2,ans = INT_MAX;
  if(x <= mid) ans = min(ans,find(id * 2,l,mid,x,y));
  if(y > mid) ans = min(ans,find(id * 2 + 1,mid + 1,r,x,y));
  return ans;
}
void gexi(int k,int id,int l,int r,int num)
{
  if(l == r)
  {
    tr[k] = num;
    return ;
  }
  int mid = (l + r) / 2;
  if(id <= mid)
    gexi(k * 2,id,l,mid,num);
  else
    gexi(k * 2 + 1,id,mid + 1,r,num);
  tr[k] = min(tr[k * 2],tr[k * 2 + 1]);
}
signed main()
{
  int n,m;
  cin>>m>>n;
  for(int i = 1; i <= m; i++) cin>>a[i];
  bui(1,1,m);
  while(n--)
  {
    int x,y,p;
    cin>>p>>x>>y;
    if(p == 1) cout<

四,结语

怎么样?看懂了吗?看懂了的话请留个赞吖!

超详解线段树(浅显易懂)_第6张图片

你可能感兴趣的:(树,数据结构,算法,c++)