[算法]线段树

Posted hdsmwww

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[算法]线段树相关的知识,希望对你有一定的参考价值。

拖了好久才写的线段树......

大概听说它可能实在n年前,在我还是一个孩子的时候[/微笑]

恩大概我觉得有一丢丢丢像分块

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

 

一 概述

线段树,类似区间树,它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(logn)。

线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间,例如父亲的区间是[a,b],那么(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b]。

二 从一个例子理解线段树

下面我们从一个经典的例子来了解线段树,问题描述如下:从数组arr[0...n-1]中查找某个数组某个区间内的最小值,其中数组大小固定,但是数组中的元素的值可以随时更新。

对这个问题一个简单的解法是:遍历数组区间找到最小值,时间复杂度是O(n),额外的空间复杂度O(1)。当数据量特别大,而查询操作很频繁的时候,耗时可能会不满足需求。

另一种解法:使用一个二维数组来保存提前计算好的区间[i,j]内的最小值,那么预处理时间为O(n^2),查询耗时O(1), 但是需要额外的O(n^2)空间,当数据量很大时,这个空间消耗是庞大的,而且当改变了数组中的某一个值时,更新二维数组中的最小值也很麻烦。

我们可以用线段树来解决这个问题:预处理耗时O(n),查询、更新操作O(logn),需要额外的空间O(n)。根据这个问题我们构造如下的二叉树

  • 叶子节点是原始组数arr中的元素
  • 非叶子节点代表它的所有子孙叶子节点所在区间的最小值

例如对于数组[2, 5, 1, 4, 9, 3]可以构造如下的二叉树(背景为白色表示叶子节点,非叶子节点的值是其对应数组区间内的最小值,例如根节点表示数组区间arr[0...5]内的最小值 是1):                                                                                                                           本文地址

由于线段树的父节点区间是平均分割到左右子树,因此线段树是完全二叉树,对于包含n个叶子节点的完全二叉树,它一定有n-1个非叶节点,总共2n-1个节点,因此存储线段是需要的空间复杂度是O(n)。那么线段树的操作:创建线段树、查询、节点更新 是如何运作的呢(以下所有代码都是针对求区间最小值问题)?

 

 

 1 struct SegTreeNode
 2 {
 3     int val;
 4 }segTree[10010];
 5 //建建建
 6 void build(int root,int arr[],int istart,iend)
 7 {
 8     if(istart==iend)
 9         segTree[root].val=arr[istart];
10     else
11     {
12         int mid=(istart+iend)/2;
13         build(root*2+1,arr,istart,mid);
14         build(root*2+2,arr,mid+1,iend);
15         segTree[root].val=min(segTree[root*2+1].val,segTree[root*2+2].val);
16     }
17 }
18 //单点查询
19 int query(int root,int nstart,int nend,int qstart,int qend)
20 {
21     if(nstart>qend || nend<qstart)
22         return INFINITE;
23     if(qstart<=nstart && qend>=nend)
24         return segTree[root].val;
25     int mid=(nstart+nend)/2;
26     return min(query(root*2+1,nstart,mid,qstart,qend),query(root*2+2,mid+1,nend,qstart,qend));
27 }
28 //单点更新
29 void update(int root,int nstart,int nend,int index,int addval)
30 {
31     if(nstart==nend)
32     {
33         if(nstart==index)segTree[index].val+=addval;
34         return;
35     }
36     int mid=(nstart+nend)/2;
37     if(index<=mid)update(root*2+1,nstart,mid,index,addval);
38     else update(root*2+2,mid+1,nend,index,addval);
39     segTree[root].val=min(segTree[2*root+1].val,segTree[2*root+2].val);
40 }

 

以及区间

  1 const int INFINITE = INT_MAX;
  2 const int MAXNUM = 1000;
  3 struct SegTreeNode
  4 {
  5     int val;
  6     int addMark;//延迟标记
  7 }segTree[MAXNUM];//定义线段树
  8 
  9 /*
 10 功能:构建线段树
 11 root:当前线段树的根节点下标
 12 arr: 用来构造线段树的数组
 13 istart:数组的起始位置
 14 iend:数组的结束位置
 15 */
 16 void build(int root, int arr[], int istart, int iend)
 17 {
 18     segTree[root].addMark = 0;//----设置标延迟记域
 19     if(istart == iend)//叶子节点
 20         segTree[root].val = arr[istart];
 21     else
 22     {
 23         int mid = (istart + iend) / 2;
 24         build(root*2+1, arr, istart, mid);//递归构造左子树
 25         build(root*2+2, arr, mid+1, iend);//递归构造右子树
 26         //根据左右子树根节点的值,更新当前根节点的值
 27         segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
 28     }
 29 }
 30 
 31 /*
 32 功能:当前节点的标志域向孩子节点传递
 33 root: 当前线段树的根节点下标
 34 */
 35 void pushDown(int root)
 36 {
 37     if(segTree[root].addMark != 0)
 38     {
 39         //设置左右孩子节点的标志域,因为孩子节点可能被多次延迟标记又没有向下传递
 40         //所以是 “+=”
 41         segTree[root*2+1].addMark += segTree[root].addMark;
 42         segTree[root*2+2].addMark += segTree[root].addMark;
 43         //根据标志域设置孩子节点的值。因为我们是求区间最小值,因此当区间内每个元
 44         //素加上一个值时,区间的最小值也加上这个值
 45         segTree[root*2+1].val += segTree[root].addMark;
 46         segTree[root*2+2].val += segTree[root].addMark;
 47         //传递后,当前节点标记域清空
 48         segTree[root].addMark = 0;
 49     }
 50 }
 51 
 52 /*
 53 功能:线段树的区间查询
 54 root:当前线段树的根节点下标
 55 [nstart, nend]: 当前节点所表示的区间
 56 [qstart, qend]: 此次查询的区间
 57 */
 58 int query(int root, int nstart, int nend, int qstart, int qend)
 59 {
 60     //查询区间和当前节点区间没有交集
 61     if(qstart > nend || qend < nstart)
 62         return INFINITE;
 63     //当前节点区间包含在查询区间内
 64     if(qstart <= nstart && qend >= nend)
 65         return segTree[root].val;
 66     //分别从左右子树查询,返回两者查询结果的较小值
 67     pushDown(root); //----延迟标志域向下传递
 68     int mid = (nstart + nend) / 2;
 69     return min(query(root*2+1, nstart, mid, qstart, qend),
 70                query(root*2+2, mid + 1, nend, qstart, qend));
 71 
 72 }
 73 
 74 /*
 75 功能:更新线段树中某个区间内叶子节点的值
 76 root:当前线段树的根节点下标
 77 [nstart, nend]: 当前节点所表示的区间
 78 [ustart, uend]: 待更新的区间
 79 addVal: 更新的值(原来的值加上addVal)
 80 */
 81 void update(int root, int nstart, int nend, int ustart, int uend, int addVal)
 82 {
 83     //更新区间和当前节点区间没有交集
 84     if(ustart > nend || uend < nstart)
 85         return ;
 86     //当前节点区间包含在更新区间内
 87     if(ustart <= nstart && uend >= nend)
 88     {
 89         segTree[root].addMark += addVal;
 90         segTree[root].val += addVal;
 91         return ;
 92     }
 93     pushDown(root); //延迟标记向下传递
 94     //更新左右孩子节点
 95     int mid = (nstart + nend) / 2;
 96     update(root*2+1, nstart, mid, ustart, uend, addVal);
 97     update(root*2+2, mid+1, nend, ustart, uend, addVal);
 98     //根据左右子树的值回溯更新当前节点的值
 99     segTree[root].val = min(segTree[root*2+1].val, segTree[root*2+2].val);
100 }

以上是关于[算法]线段树的主要内容,如果未能解决你的问题,请参考以下文章

算法模板——线段树

算法xio讲堂#2--线段树

线段树基础

(使用线段树实现的)扫描线算法

算法线段树

算法模板-线段树