• 线段树学习


    1概述

    线段树,也叫区间树,是一个完全二叉树,它在各个节点保存一条线段(即"子数组"),因而常用于解决数列维护问题,它基本能保证每个操作的复杂度为O(lgN)

    线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

        对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

        使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。

       性质:父亲的区间是[a,b],(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b],线段树需要的空间为数组大小的四倍。

    关于线段树4倍空间的解释:

    用数组表达的线段树的基本性质:

    1、树根从1开始,即tree[0]不用。

    2、若根下标是p,则左子下标是2p,右子下标是2p+1。

    3、结点的线段不能显式给出(树根除外),需要从树根开始往下计算。比如知道树根线段是[1,N],左子结点线段可计算得到[1,(1+N)/2]。

    4、[1,2^n]的线段树需要的数组长度是(2^(n+1)-1)+1=2^(n+1),包括了下标为0的数组元素。

    5、对于任意大小的N,N<=MAX,MAX为定义的最大线段长度,为保证树的数组不溢出,保险的做法是令树的数组长度为4*MAX。说明如下:

    当N=2^n时,无疑2^(n+1)已经够用,即2*MAX已经足够。

    当N!=2^n时,令2^n<N<2^(n+1),那么树的数组长度最大能达到2^(n+2)(由N的上限算出),而N的下限是2^n,则最大相差4倍,所以4*MAX能保证不溢出。

    2线段树的存储结构

    由上面的图可以看出,存储一颗线段树和二叉树有点类似,需要左孩子和右孩子节点,另外,为了存储每条线段出现的次数,所以一般会加上计数的元素,其具体如下:

    struct Node         // 线段树

    {

    int left;

    int right;

    int counter;

    }segTree[4*BORDER]; 

       

    3线段树基本操作

    线段树的基本操作主要包括构造线段树,区间查询和区间修改

    1. void construct(int index, int lef, int rig),构建线段树 根节点开始构建区间[lef,rig]的线段树
    2. void insert(int index, int start, int end),插入线段[start,end]到线段树, 同时计数区间次数
    3. int query(int index, int x),查询点x的出现次数,从根节点开始到[x,x]叶子的这条路径中所有点计数相加方为x出现次数
    4. void delete_ (int c , int d, int index),从线段树中删除线段[c,d]

       

    1)  线段树构造

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

       

    线段树除了最后一层外,前面每一层的结点都是满的,因此线段树的深度

    h =ceil(log(2n -1))=O(log n)。

    代码:

    /* 构建线段树 根节点开始构建区间[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;

    }

    2)  区间查询

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

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

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

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

    /* 查询点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);

    }

       

    3)  线段树元素插入

    代码:

    /* 插入线段[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);

    }

    }

       

    4)  线段树元素删除

    代码:

    void  delete_ (int c , int  d, int index)

    {

           if(c <= segTree[index].left && d >= segTree[index].right) 

               segTree[index].counter--;

           else 

           {

              if(c < (segTree[index].left + segTree[index].right)/2 ) delete_( c,d, segTree[index].left);

              if(d > (segTree[index].left + segTree[index].right)/2 ) delete_( c,d, segTree[index].right);

           }

       

    4应用

    下面给出线段树的几个应用:

    (1)有一列数,初始值全部为0。每次可以进行以下三种操作中的一种:

    a. 给指定区间的每个数加上一个特定值;

    b.将指定区间的所有数置成一个统一的值;

    c.询问一个区间上的最小值、最大值、所有数的和。

    给出一系列a.b.操作后,输出c的结果。

    [问题分析]

    这个是典型的线段树的应用。在每个节点上维护一下几个变量:delta(区间增加值),same(区间被置为某个值),min(区间最小值),max(区间最大值),sum(区间和),其中delta和same属于"延迟标记"。

    (2)在所有不大于30000的自然数范围内讨论一个问题:已知n条线段,把端点依次输入给你,然后有m(30000)个询问,每个询问输入一个点,要求这个点在多少条线段上出现过。

    [问题分析]

    在这个问题中,我们可以直接对问题处理的区间建立线段树,在线段树上维护区间被覆盖的次数。将n条线段插入线段树,然后对于询问的每个点,直接查询被覆盖的次数即可。

    但是我们在这里用这道题目,更希望能够说明一个问题,那就是这道题目完全可以不用线段树。我们将每个线段拆成(L,+1),(R+1,-1)的两个事件点,每个询问点也在对应坐标处加上一个询问的事件点,排序之后扫描就可以完成题目的询问。我们这里讨论的问题是一个离线的问题,因此我们也设计出了一个很简单的离线算法。线段树在处理在线问题的时候会更加有效,因为它维护了一个实时的信息。

    这个题目也告诉我们,有的题目尽管可以使用线段树处理,但是如果我们能够抓住题目的特点,就可能获得更加优秀的算法。

    (3)某次列车途经C个城市,城市编号依次为1到C,列车上共有S个座位,铁路局规定售出的车票只能是坐票,即车上所有的旅客都有座,售票系统是由计算机执行的,每一个售票申请包含三个参数,分别用O、D、N表示,O为起始站,D为目的地站,N为车票张数,售票系统对该售票申请作出受理或不受理的决定,只有在从O到D的区段内列车上都有N个或N个以上的空座位时该售票申请才被受理,请你写一个程序,实现这个自动售票系统。

    [问题分析]

    这里我们可以把所有的车站顺次放在一个数轴上,在数轴上建立线段树,在线段树上维护区间的delta与max。每次判断一个售票申请是否可行就是查询区间上的最大值;每个插入一个售票请求,就是给一个区间上所有的元素加上购票数。

    这道题目在线段树上维护的信息既包括自下至上的递推,也包括了自上至下的传递,能够比较全面地对线段树的基本操作进行训练。

    (4)给一个n*n的方格棋盘,初始时每个格子都是白色。现在要刷M次黑色或白色的油漆。每次刷漆的区域都是一个平行棋盘边缘的矩形区域。

    输入n,M,以及每次刷漆的区域和颜色,输出刷了M次之后棋盘上还有多少个棋格是白色。

    [问题分析]

    首先我们从简单入手,考虑一维的问题。即对于一个长度为n的白色线段,对它进行M次修改(每次更新某一子区域的颜色)。问最后还剩下的白色区域有多长。

    对于这个问题,很容易想到建立一棵线段树的模型。复杂度为O(Mlgn)。

    扩展到二维,需要把线段树进行调整,即首先在横坐标上建立线段树,它的每个节点是一棵建立在纵坐标上的线段树(即树中有树。称为二维线段树)。复杂度为O(M(logn)^2)。

    ACM练习题

    HDOJ 1166 敌兵布阵(http://acm.hdu.edu.cn/showproblem.php?pid=1166)

    AC代码:

    HDOJ 1754 Ihate it (http://acm.hdu.edu.cn/showproblem.php?pid=1754)

    AC代码:

    //HDOJ 1754 I Hate It(http://acm.hdu.edu.cn/showproblem.php?pid=1754)

    //线段树的应用

    #include <iostream>

    #include <cstdio>

       

    using namespace std;

       

    const int MA_LEN = 200010;

    //线段树需要4倍的存储空间

    int sum[4 * MA_LEN];

       

    //函数:构建线段树

    void construct(int index,int left,int right);

    //函数:更新线段树

    void update(int index,int left,int right,int flag,int num);

    //函数:查询线段树

    int query(int index,int left,int right,int l,int r);

       

    int main(){

    int N,M;

    while(cin >> N){

    cin >> M;

       

    construct(1,1,N);

       

    char ch;

    int l,r;

    while(M--){

    getchar();

    scanf("%c%d%d",&ch,&l,&r);

    if ('U' == ch)

    update(1,1,N,l,r);

    else if ('Q' == ch)

    {

    cout << query(1,1,N,l,r) << endl;

    }

    }

    }

       

    return 0;

    }

       

    //函数:构建线段树

    //参数:

    //index:当前线段树节点的索引

    //left、right:当前线段树节点表示的区间

    void construct(int index,int left,int right){

    int val;

    if (left == right)

    {

    cin >> val;

    sum[index] = val;

    }

    else{

    int mid = (left + right) >> 1;

    //递归构建左孩子

    construct(index * 2,left,mid);

    //递归构建右孩子

    construct(index * 2 + 1,mid + 1,right);

    //根节点中存储最大的数,是左孩子中最大的数和右孩子中最大的数这两个数中较大的数

    sum[index] = sum[index * 2] > sum[index * 2 + 1] ? sum[index * 2] : sum[index * 2 + 1];

    }

    }

       

    //函数:更新线段树

    //参数:

    //index:当前线段树节点索引

    //left,right当前线段树节点表示的区间

    //flag:要修改的区间中的某一点

    //num:要改成的目标值

    void update(int index,int left,int right,int flag,int num){

    if (left == right)

    {

    sum[index] = num;

    }

    else{

    int mid = (left + right) >> 1;

    if (flag <= mid)

    {

    update(index * 2,left,mid,flag,num);

    }

    else

    update(index * 2 + 1,mid + 1,right,flag,num);

       

    sum[index] = sum[index * 2] > sum[index * 2 + 1] ? sum[index * 2] : sum[index * 2 + 1];

    }

    }

       

    //函数:查询线段树

    //参数:

    //index:当前线段树节点索引

    //left、right:当前线段树节点表示的区间

    //l、r:要查询的区间

    int query(int index,int left,int right,int l,int r){

    if (left == l && right == r)

    {

    return sum[index];

    }

    else{

    int mid = (left + right) >> 1;

    //在左子树中递归查找

    if (r <= mid)

    {

    query(index * 2,left,mid,l,r);

    }

    //在右子树中递归查找

    else if (l > mid)

    {

    query(index * 2 + 1,mid + 1,right,l,r);

    }

    //分成两部分

    else{

    int a = query(index * 2,left,mid,l,mid);

    int b = query(index * 2 + 1,mid + 1,right,mid + 1,r);

    return (a > b ? a : b);

    }

    }

    }

       

    4总结

    利用线段树,我们可以高效地询问和修改一个数列中某个区间的信息,并且代码也不算特别复杂。

    但是线段树也是有一定的局限性的,其中最明显的就是数列中数的个数必须固定,即不能添加或删除数列中的数。

    5参考资料

    (1) 杨弋文章:《线段树》:

    http://download.csdn.net/source/2255479

    (2) 林涛文章《线段树的应用》:

    http://wenku.baidu.com/view/d65cf31fb7360b4c2e3f64ac.html

    (3) 朱全民文章《线段树及其应用》:

    http://wenku.baidu.com/view/437ad3bec77da26925c5b0ba.html

    (4)  线段树:

    http://wenku.baidu.com/view/32652a2d7375a417866f8f51.html

    以上部分来自董的空间 http://dongxicheng.org/structure/segment-tree/

    http://blog.csdn.net/w397090770/article/details/8219727

  • 相关阅读:
    AutoCAD 2013 .net插件创建向导现在支持Map 3D,Civil 3D,AutoCAD Architecture 和AutoCAD MEP
    AutoCAD® Civil 3D API需求意愿调查
    Linux 下动态库和静态库的创建和调用
    几个典型的内存拷贝及字符串函数实现
    典型的几个链表操作-逆序和重排
    打印 N*N 螺旋矩阵
    PhoneGap开发初体验:用HTML5技术开发本地应用
    不申请变量和空间反转字符串
    寻找最大公共子字符串
    二维动态数组定义及二维静态数组与**P的区别
  • 原文地址:https://www.cnblogs.com/fuyou/p/3233315.html
Copyright © 2020-2023  润新知