zoukankan      html  css  js  c++  java
  • 线段树

    首先提出一个问题: 
    给你n个数,有两种操作:
    1:给第i个数的值增加X

    2:询问区间[a,b]的总和是什么?


    输入描述

    输入文件第一行为一个整数n,接下来是n行n个整数,表示格子中原来的整数。接下一个正整数q,再接
    下来有q行,表示q个询问,第一个整数表示询问代号,询问代号1表示增加,后面的两个数x和A表示给
    位置X上的数值增加A,询问代号2表示区间求和,后面两个整数表示a和b,表示要求[a,b]之间的区间和。


    样例输入

    4


    7 6 3 5


    2


    1 1 4


    2 1 2


    样例输出

    17


    数据范围


    1 <= n,q <= 100000


    看到这个问题,最朴素的想法是用一个数组模拟,求和时 [ a , b ]中逐个累加 , 最后输出 。
    但是,由于数据量比较大,时间复杂度太高,时间上无法承受。


    这时我们可以用线段树( Segment Tree ),这种特殊的数据结构解决这个问题。


    那么什么是线段树呢?

    这就是一棵典型的线段树

    一 般的线段树上的每一个节点T[a , b],代表该节点维护了原数列[ a , b ]区间的信息。对于每一个节点他至少有
    三个信息:左端点,右端点,我们需要维护的信息(在本题中我们维护区间和)。由于线段树是一个二叉树,而且是一个平衡二叉树,如果当前结点的编号是i,左端点为L ,右端点为 R , 那么左儿子的 编号为 i*2 ,左端点为 L ,右端点为 (L + R)/2 ; 同理右儿子的 编号为 i*2+1,左端点为(L+R)/2 ,右端点为 R
    。如果当前结点的左端点等于右端点,那么该节点就是叶子节点,直接在该节点赋值即可。显然线段树是递归定义的。

    线段树就是这样一种数据结构,讲一个大区间分为若干个不相交的区间,每次维护都在小区间上处理,并且查


    询也在这些被分解的区间中信息合并出我们需要的结果,这就是线段树高效的原因。

    线段树的存储:


    线段树的存储可用链表和数组模拟。(本文采用数组写法,便于理解)


    1.链表存储:

    struct node
    {
         int Left, Right;
         node *Leftchild , *Rightchild;
    };

    2.数组模拟

    struct Tree
    {
    
        int l, r;
        int sum;
    
    } tr[maxN*4];

    注意:数组的空间要开四倍大小,防止访问越界。(理论上是2n-1的空间,但是递归建立树的时候,我们是从根出发向下递归, 当前节点为r,左右孩子分别是2*r,2*r+1。 由于取中间值mid,将区间平分的过程中,区间长度可能是单数,造成左右区间[left, mid],[mid+1, right] 的长度不同,必然最后存在某一层有的节点可以再分,有的却不能再分,从而致使整棵树并不是像上图那样,所有分支都有叶子节点。 这样一来有一些数组的位置就被浪费了,所以实际数组大小要大于2n-1 )

    建树:

    线段树的构建是自顶点而下,即从根节点开始递归构建,根据线段树定义,当左端点等于右端点时(达到递归边界),直接赋值即可,回溯时也要维护区间,代码如下:

    void Build_Tree(int left, int right, int root)
    {
    
        tr[root].l = left;
        tr[root].r = right;
    
        if (left == right)tr[root].sum = a[left]; //找到叶子节点,赋值
    
        else
        {
            int mid = (tr[root].l + tr[root].r) / 2;
    
            Build_Tree(left, mid, root * 2); //左子树
    
            Build_Tree(mid + 1, right, root * 2 + 1); //右子树
    
            tr[root].sum = tr[root * 2].sum + tr[root * 2 + 1].sum; //回溯维护区间和
        }
    }

    维护树:


    维护树的方法也很好理解,如果目标更新节点在左儿子里,去左儿子中查找;反之,在右儿子中。不断递归,知道找到需要维护的节点,更新它,回溯是一路更新回来。这就是维护的过程,代码如下:

    void Update_Tree(int q, int val, int root)
    {
        if (tr[root].l == q && tr[root].r == q) //找到需要修改的叶子节点
        {
            tr[root].sum = val; //更新当前结点
        }
        else //当前结点是非叶子结点
        {
            int mid = (tr[root].l + tr[root].r) / 2; //取中间
    
            if (q <= mid) //目标节点在左儿子中
            {
                Update_Tree(q, val, root * 2);
            }
            else if (q > mid) //目标节点在右儿子中
            {
                Update_Tree(q, val, root * 2 + 1);
            }
            tr[root].sum = tr[root * 2].sum + tr[root * 2 + 1].sum; //回溯
        }
    }

    查询树:


    题目中让我们查询区间求和,不难想到如果当前结点的区间完全被目标区间包含,直接返回当前结点的sum值,

    否则分类讨论。具体过程通过以下代码理解:

    int Query_Tree(int left, int right, int i)
    {
        if (left <= tr[i].l && right >= tr[i].r) return tr[i].sum; //当前结点的区间完全被目标区间包含
        else
        {
            int mid = (tr[i].l + tr[i].r)/2;
            if (left > mid) //完全在右儿子
            {
                return Query_Tree(left, right, i*2+1);
            }
            else if (right <= mid) //完全在左儿子
            {
                return Query_Tree(left, right, i*2);
            }
            else //目标区间在左右都有分布
            {
                return Query_Tree(left, right, i*2) + Query_Tree(left, right, i*2+1);
            }
        }
    }

    主程序:

    int main()
    {
    
        int q, val, l, r;
        cout << "输入10个数字" << endl;
        for (int i = 1; i < maxN+1; i++)
            cin >> a[i];
        Build_Tree(1, maxN, 1);
        int op;
        while (true)
        {
            cout << "输入操作编号(1[update]  2[query]  0[end])" << endl;
            cin >> op;
            if (op == 1)
            {
                cout << "更新操作, 输入: 更新位置, 更新值" << endl;
                cin >> q >> val;
                Update_Tree(q, val, 0);
    
            }
            else if (op == 2)
            {
                cout << "查询操作, 输入: 起始位置, 结束位置" << endl;
                cin >> l >> r;
                cout << Query_Tree(l, r, 0) << endl;
            }
            else
            {
                break;
            }
        }
        return 0;
    }

    线段树的性质:


    假设线段树处理的数列长度为N,那么总结点数不超过2*N(满二叉树是最大情况);

    线段分解数量级:线段树能把任意一条长度为M的线段分为不超过2Log2(M)条线段(我们知道一个很大的数,Log一下就变小了),这条性质使线段树的查询与修改复杂度都在O(Log2(n))的范围内解决。


    由于线段树是一颗二叉树,深度约为Log2(N)左右。


    综上,线段树空间消耗O(n),由于它深度性质,使它在解决问题上有较高的效率。

  • 相关阅读:
    实验 4:Open vSwitch 实验——Mininet 中使用 OVS 命令
    实验 2:Mininet 实验——拓扑的命令脚本生成
    软工第一次作业——自我介绍
    博客园美化
    实验 1:Mininet 源码安装和可视化拓扑工具
    软工实践个人总结
    结对编程之学术家族树
    软件工程实践结对编程作业(需求分析与原型设计)
    软件工程实践个人编程作业
    软件工程实践第一次个人作业
  • 原文地址:https://www.cnblogs.com/scarecrow-blog/p/6429150.html
Copyright © 2011-2022 走看看