线段树

来源:互联网 发布:2017剑灵捏脸数据龙女 编辑:程序博客网 时间:2024/05/18 08:10
线段树
1.概述
       线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即“子数组”),因而常用于解决数列维护问题,它基本能保证每个操作的复杂度为O(lgN)。
2.线段树基本操作
      线段树的基本操作主要包括构造线段树,区间查询和区间修改.
3.线段树的相关操作代码

(1)    线段树构造

首先介绍构造线段树的方法:让根节点表示区间[0,N-1],即所有N个数所组成的一个区间,然后,把区间分成两半,分别由左右子树表示。不难证明,这样的线段树的节点数只有2N-1个,是O(N)级别的,如图:

//构造求解区间最小值的线段树

function 构造以v为根的子树
 
  ifv所表示的区间内只有一个元素
 
     v区间的最小值就是这个元素, 构造过程结束
 
  endif
 
  把v所属的区间一分为二,用w和x两个节点表示。
 
  标记v的左儿子是w,右儿子是x
 
  分别构造以w和以x为根的子树(递归)
 
  v区间的最小值 <- min(w区间的最小值,x区间的最小值)
 
end function

线段树除了最后一层外,前面每一层的结点都是满的,因此线段树的深度h =ceil(log(2n -1))=O(log n)。

(2)    区间查询

区间查询指用户输入一个区间,获取该区间的有关信息,如区间中最大值,最小值,第N大的值等。

比如前面一个图中所示的树,如果询问区间是[0,2],或者询问的区间是[3,3],不难直接找到对应的节点回答这一问题。但并不是所有的提问都这么容易回答,比如[0,3],就没有哪一个节点记录了这个区间的最小值。当然,解决方法也不难找到:把[0,2]和[3,3]两个区间(它们在整数意义上是相连的两个区间)的最小值“合并”起来,也就是求这两个最小值的最小值,就能求出[0,3]范围的最小值。同理,对于其他询问的区间,也都可以找到若干个相连的区间,合并后可以得到询问的区间。

区间查询的伪代码如下:

// node 为线段树的结点类型,其中Left 和Right 分别表示区间左右端点
// Lch 和Rch 分别表示指向左右孩子的指针
void Query(node *p, int a, int b) // 当前考察结点为p,查询区间为(a,b]
{
  if (a <= p->Left && p->Right <= b)
  // 如果当前结点的区间包含在查询区间内
  {
     ...... // 更新结果
     return;
  }
  Push_Down(p); // 等到下面的修改操作再解释这句
  int mid = (p->Left + p->Right) / 2; // 计算左右子结点的分隔点
  if (a < mid) Query(p->Lch, a, b); // 和左孩子有交集,考察左子结点
  if (b > mid) Query(p->Rch, a, b); // 和右孩子有交集,考察右子结点
}

可见,这样的过程一定选出了尽量少的区间,它们相连后正好涵盖了整个[l,r],没有重复也没有遗漏。同时,考虑到线段树上每层的节点最多会被选取2个,一共选取的节点数也是O(log n)的,因此查询的时间复杂度也是O(log n)。

线段树并不适合所有区间查询情况,它的使用条件是“相邻的区间的信息可以被合并成两个区间的并区间的信息”。即问题是可以被分解解决的。

(3)    区间修改

当用户修改一个区间的值时,如果连同其子孙全部修改,则改动的节点数必定会远远超过O(log n)个。因而,如果要想把区间修改操作也控制在O(log n)的时间内,只修改O(log n)个节点的信息就成为必要。

借鉴前一节区间查询用到的思路:区间修改时如果修改了一个节点所表示的区间,也不用去修改它的儿子节点。然而,对于被修改节点的祖先节点,也必须更新它所记录的值,否则查询操作就肯定会出问题(正如修改单个节点的情况一样)。

这些选出的节点的祖先节点直接更新值即可,而选出的节点的子孙却显然不能这么简单地处理:每个节点的值必须能由两个儿子节点的值得到,如这幅图中的例子:

这里,节点[0,1]的值应该是4,但是两个儿子的值又分别是3和5。如果查询[0,0]区间的RMQ,算出来的结果会是3,而正确答案显然是4。

问题显然在于,尽管修改了一个节点以后,不用修改它的儿子节点,但是它的儿子节点的信息事实上已经被改变了。这就需要我们在节点里增设一个域:标记。把对节点的修改情况储存在标记里面,这样,当我们自上而下地访问某节点时,就能把一路上所遇到的所有标记都考虑进去。

但是,在一个节点带上标记时,会给更新这个节点的值带来一些麻烦。继续上面的例子,如果我把位置0的数字从4改成了3,区间[0,0]的值应该变回3,但实际上,由于区间[0,1]有一个“添加了1”的标记,如果直接把值修改为3,则查询区间[0,0]的时候我们会得到3+1=4这个错误结果。但是,把这个3改成2,虽然正确,却并不直观,更不利于推广(参见下面的一个例子)。

为此我们引入延迟标记的一些概念。每个结点新增加一个标记,记录这个结点是否被进行了某种修改操作(这种修改操作会影响其子结点)。还是像上面的一样,对于任意区间的修改,我们先按照查询的方式将其划分成线段树中的结点,然后修改这些结点的信息,并给这些结点标上代表这种修改操作的标记。在修改和查询的时候,如果我们到了一个结点p ,并且决定考虑其子结点,那么我们就要看看结点p 有没有标记,如果有,就要按照标记修改其子结点的信息,并且给子结点都标上相同的标记,同时消掉p 的标记。代码框架为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// node 为线段树的结点类型,其中Left 和Right 分别表示区间左右端点
 
// Lch 和Rch 分别表示指向左右孩子的指针
 
voidChange(node *p, inta,intb)// 当前考察结点为p,修改区间为(a,b]
 
{
 
  if(a <= p->Left && p->Right <= b)
 
  // 如果当前结点的区间包含在修改区间内
 
  {
 
     ......// 修改当前结点的信息,并标上标记
 
     return;
 
  }
 
  Push_Down(p);// 把当前结点的标记向下传递
 
  intmid = (p->Left + p->Right) / 2; // 计算左右子结点的分隔点
 
  if(a < mid) Change(p->Lch, a, b); // 和左孩子有交集,考察左子结点
 
  if(b > mid) Change(p->Rch, a, b); // 和右孩子有交集,考察右子结点
 
  Update(p);// 维护当前结点的信息(因为其子结点的信息可能有更改)

4.线段树求区间最大值
#include <iostream>
using namespace std;
/* 线段树求区间最大值 */
#define MAXN 100   /* 最大元素个数 */
#define MAX(a,b) ((a) > (b) ? (a) : (b))

struct Node
{
int left;
int right;
int maxV;
}segTree[4*MAXN];  /* 线段树 */

int arr[] = {56,23,10,78,9,
100,26,52,30,18};   /* 测试数组 */ 

/* 根据测试数组建立线段树 根节点开始构造[left,right]区间最大值*/
void construct(int index, int left, int right)
{
segTree[index].left = left;
segTree[index].right = right;
if(left == right)  // 叶子,即为该区间最大值,构造结束
{
segTree[index].maxV = arr[left];
return;
}
int mid = (left + right) >> 1;
construct((index<<1)+1, left, mid);
construct((index<<1)+2, mid+1, right);
segTree[index].maxV = MAX(segTree[(index<<1)+1].maxV,segTree[(index<<1)+2].maxV);
}

/* 线段树从根节点查询某个区间的最大值 */
int query(int index, int left, int right)
{
if(segTree[index].left == left && segTree[index].right == right)
{
return segTree[index].maxV;
}
int mid = (segTree[index].left+segTree[index].right) >> 1;
if(right <= mid)     // 注:此区间为[0,mid] 故 <=
{
return query((index<<1)+1,left,right);
}else if(left > mid) // 注:此区间为[mid+1,right] 故 >
{
return query((index<<1)+2,left,right);
}
return MAX(query((index<<1)+1,left,mid), query((index<<1)+2,mid+1,right));
}

void main()
{
construct(0,0,sizeof arr / sizeof(int) - 1);
// 注:construct和query函数省略了边界检查
printf("max of [0,9] is: %d\n",query(0,0,9));
printf("max of [1,4] is: %d\n",query(0,1,4));
printf("max of [3,7] is: %d\n",query(0,3,7));
printf("max of [6,9] is: %d\n",query(0,6,9));
}


5. 线段树求点在线段中的出现次数
#include<iostream>
using namespace std;

/* 线段树求点在已知线段中的出现次数 */
#define BORDER 100  // 设线段端点坐标不超过100
struct Node         // 线段树
{
int left;
int right;
int counter;
}segTree[4*BORDER];  

/* 构建线段树 根节点开始构建区间[lef,rig]的线段树*/
void construct(int index, int lef, int rig)
{
segTree[index].left = lef;
segTree[index].right = rig;
if(lef == rig)   // 叶节点
{
segTree[index].counter = 0;
return;
}
int mid = (lef+rig) >> 1;
construct((index<<1)+1, lef, mid);
construct((index<<1)+2, mid+1, rig);
segTree[index].counter = 0;
}

/* 插入线段[start,end]到线段树, 同时计数区间次数 */
void insert(int index, int start, int end)
{
if(segTree[index].left == start && segTree[index].right == end)
{
++segTree[index].counter;
return;
}
int mid = (segTree[index].left + segTree[index].right) >> 1;
if(end <= mid)
{
insert((index<<1)+1, start, end);
}else if(start > mid)
{
insert((index<<1)+2, start, end);
}else
{
insert((index<<1)+1, start, mid);
insert((index<<1)+2, mid+1, end);
}
}

/* 查询点x的出现次数 
 * 从根节点开始到[x,x]叶子的这条路径中所有点计数相加方为x出现次数
 */
int query(int index, int x)
{
if(segTree[index].left == segTree[index].right) // 走到叶子,返回
{
return segTree[index].counter;
}
int mid = (segTree[index].left+segTree[index].right) >> 1;
if(x <= mid)
{
return segTree[index].counter + query((index<<1)+1,x);
}
return segTree[index].counter + query((index<<1)+2,x);
}

/* 测试线段 */
int segment[10][2] = {
5, 8, 10, 45,  0, 7,
2, 3, 3, 9, 13, 26,
   15, 38,  50, 67,   39, 42, 
   70, 80 
};
/* 测试点 :answer: 1,2,2,3,1,3,2,1,0,0 */
int testPoint[10] = {
1, 2, 4, 6, 9,
  15, 13, 44, 48, 90
};

void main()
{
construct(0,0,100);           // 构建[0,100]线段树
for(int j = 0; j < 10; ++j)   // 插入测试线段
{
insert(0,segment[j][0],segment[j][1]);
}
for(int i = 0; i < 10; ++i)   // 查询点出现次数
{
printf("frequent of point %d is: %d\n", 
  testPoint[i], query(0,testPoint[i]));
}
}

6. 线段覆盖长度
#include<iostream>
using namespace std;
 
/* 排序求线段覆盖长度 */
#define MAXN 100   // 设线段数不超过100
 
struct segment
{
    int start;
    int end;
}segArr[100];
 
/* 计算线段覆盖长度 */
int lenCount(segment * segArr, int size)
{
    int length = 0, start = 0, end = 0;
    for(int i = 0; i < size; ++i)
    {
        start = segArr[i].start;
        end = segArr[i].end;
        while(end >= segArr[i+1].start)
        {
            ++i;
            end = segArr[i].end > end ? segArr[i].end : end;
        }
        length += (end - start);
    }
    return length;
}
 
/* 快排比较函数 (从小到大进行排序)*/
int cmp(const void * p, const void *q)
{
    if(((segment *)p)->start != ((segment *)q)->start)
    {
        return ((segment *)p)->start - ((segment *)q)->start;
    }
    return ((segment *)p)->end - ((segment *)q)->end;
}
 
/* 测试线段 answer: 71 */
int segTest[10][2] = {
    5, 8,   10, 45,    0, 7,
    2, 3,    3, 9,    13, 26,
   15, 38,  50, 67,    39, 42,
   70, 80
};
 
void main()
{
    for(int i = 0; i < 10; ++i)           // 测试线段
    {
        segArr[i].start = segTest[i][0];
        segArr[i].end = segTest[i][1];
    }
    qsort(segArr,10,sizeof(segment),cmp);       // 排序
    printf("length: %d\n",lenCount(segArr,10)); // 计算
}