zoukankan      html  css  js  c++  java
  • 【数据结构】线段树

    这几天上网课学了很多东西,现在整理一下

    线段树

    有一位老师说线段树和树状数组没有什么区别,让我们只学一个就行
    我还是都学了
    现在来整理一下线段树

    进入正题

    在我这个线段树萌新的眼中,它就是用来求前缀和和统计的ㄟ( ▔, ▔ )ㄏ

    为什么叫做线段树:

    大概就是它长得很像树,然后它也是个树吧~~
    至少它都有满二叉树的特点:

    • 每个节点有一个序号 k

    • 除了叶节点其他节点都有左孩子和右孩子

    • k 节点左孩子的序号为 k * 2

    • k 节点右孩子的序号为 k * 2 + 1

    但线段树也有与众不同的特点:

    • 存储的是一个区间的信息(如区间和)

    • 大部分操作都是用二分的思想

    便于记忆,可以直接把线段树看作是一个存储着区间信息的满二叉树

    一些记忆:

    以前没有学过线段树到时候,我经常把一个数组分成两部分,然后把每一段再分,一直到分成元素不能再分为止,我还经常想为什么不能这样求前缀和,不然太不方便了,结果学了线段树后发现被打脸了……

    或许我早生个几十年……≖‿≖✧

    线段树就是这样的:

    把一个线性数组平均分成两半,然后把每一段继续分成两段,一直到只剩一个元素不能再分为止

    画个图:

    线段树

    然后把它们当做树给编个号:
    线段树1

    简化一下:
    线段树2
    现在应该就能看出来了:线段树本质其实是一个满二叉树
    除了叶节点,每个节点存储着一个区间的信息
    而叶节点存储单个元素的信息

    程序实现:

    思路:

    1. 用递归分别存储一个节点的左孩子和右孩子

    2. 在存储的同时求和

    建树代码:(我是从一位dalao的博客里学的)

    void build_tree(int l, int r, int k) {  // l 存储这个区间的左端点,r 存储这个区间的右端点,k 存储这个区间的序号
    	e[k].l = l;
    	e[k].r = r;                     // 用结构体存储一个节点的信息
    	if (l == r) {                   // 如果递归到一个叶节点
    	      e[k].sum = cun[l];        // 存下这个节点的和
    		return;                 // dalao说过这句别忘了加,不然会死循环
    	}
    	int mid = (l + r) / 2;          // 二分思想
    	build_tree(l, mid, k*2);        // 递归左孩子
    	build_tree(mid + 1, r, k*2 + 1);// 递归右孩子
    	e[k].sum = e[k*2].sum + e[k*2 + 1].sum; // 求和
    }
    

    一些线段树的基本操作:

    首先,先引入一个很有用的东西:延迟标记(也叫懒标记)

    先举个栗子 (。・ω・)ノ゙:

    有一组数:存为 a 数组,现在我要把 a[3]a[7] 中间的所有数都加上 4(包括 a[3]a[7]),最后输出 a[3]a[7] 的区间和。

    用线段树的做法怎么做?(光想思路,不用代码实现)

    暴力做法:

    用线段树将 a[3]a[7] 的每一个点都加上 4,然后再重新求有 a[3]a[7] 的所有区间的和。

    有这种思路是正常的,特别是我这种蒟蒻,但这种方法实在是太暴力了,如果做题容易 TLE,因为数据一大这种方法就跟用线段树枚举没有什么区别,甚至可能比枚举的耗时还要长……

    这个时候就是 超级飞侠 延迟标记发挥作用的时候了

    延迟标记优化做法:

    在存储树节点的结构体里再开一个变量 book
    然后递归找到(下面会说做法) a[3]a[7] 的区间并直接加上 4,并将 4 存储到那个区间的 book 里并把这个区间的 book 清零,下一步就是:

    不管了

    没错,就是不管了,反正我要求输出的是 a[3]a[7] 的区间和,又没要求输出 a[3] 的值或者什么的。

    那可能会有人想:
    如果又要求输出 a[3] 之类的东西呢?

    那这个时候就是延迟标记名字中“延迟”的由来:

    如果又要求输出 a[3],那么可以再一次递归找 a[3],然后等找到 a[3]a[7] 的左孩子或者右孩子的时候,由于 book 不为零,那么将这段区间的 sum 加上 book 中的值并把 book 继续下传然后清零。

    画个流程:

    最后到达目标区间后记得 return 结束递归哦~~

    流程大概就是这样,现在来解释一下:

    可以这样理解:假如你的假期作业还没做(比如我),其中有要求交的,也有没让交的(老师不检查),那你会选择都做完还是只做要求教的?

    反正我选二 (☆゚∀゚)

    但是你现在不知道哪些作业要交,哪些作业不用交,只有课代表开始收作业的时候你才知道,那你是不是会赶紧补那个需要交的作业(争分夺秒)?

    反正我会这样 (☆゚∀゚)

    延迟标记就可以这样理解:如果需要这个节点的信息,那么延迟标记就赶紧把这个节点的信息给更新了,然后 光(开)荣(心)退(离)休(场)~~

    就像疯狂补完该交的作业并交上去然后松了一口气的我一样(。・`ω´・)

    代码实现:

    void sign(int k){
    	e[k*2].book += e[k].book;           // 将父节点的延迟标记累加到左孩子中
    	e[k*2 + 1].book += e[k].book;       // 将父节点的延迟标记累加到右孩子中
    	e[k*2].sum += e[k].book * (e[k*2].r - e[k*2].l + 1); // 更新左孩子总和
    	e[k*2 + 1].sum += e[k].book * (e[k*2 + 1].r - e[k*2 + 1].l + 1); // 更新右孩子总和
    	e[k].book = 0;                      // 延迟标记清零
    }
    

    大约就是这个亚子

    然后各操作代码

    • 单点查询:
    void ask_point(int k) {
        if (e[k].l == e[k].r) { // 如果查询到目标节点
            ans = e[k].sum;     // 记录下该节点的值
            return ;
        }
        if (e[k].book) sign(k); // 如果这个节点的 book 值不为零,动用延迟标记
        int mid = (e[k].l + e[k].r) / 2; // 二分思想
        if (x <= mid) ask_point(k*2); // 如果目标节点在左区间,则递归左孩子
        else ask_point(k*2 + 1);// 否则递归右孩子
    }
    
    • 单点修改:
    void change_point(int k) {
        if (e[k].l == e[k].r) {  // 如果递归到目标节点
            e[k].sum += y;      // 改变该节点的值
            return;
        }
        if (e[k].book) sign(k); // 如果这个节点的 book 值不为零,动用延迟标记
        int mid = (e[k].l + e[k].r) / 2; // 二分思想
        if (x <= mid) change_point(k*2); // 如果目标节点在左区间,则递归左孩子
        else change_point(k*2 + 1); // 否则递归右孩子
        e[k].sum = e[k*2].sum + e[k*2 + 1].sum; // 重新求和
    }
    
    • 区间查询:
    void ask_line(int k) {
        if (e[k].l >= a && e[k].r <= b) {   // 如果递归的该区间为目标区间的一部分
            ans += e[k].sum;                // 累加区间和
            return;
        }
        if (e[k].book) sign(k);
        int mid = (e[k].l + e[k].r) / 2;
        if (a <= mid) ask_line(k*2);
        if (b > mid) ask_line(k*2 + 1);
    }
    
    • 区间修改:
    void change_line(int k) {
        if (e[k].l >= a && e[k].r <= b) {
            e[k].sum += y * (e[k].r - e[k].l + 1);
            e[k].book += y;
            return;
        }
        if (e[k].book) sign(k);
        int mid = (e[k].l + e[k].r) / 2;
        if (a <= mid) change_line(k*2);
        if (b > mid) change_line(k*2 + 1);
        e[k].sum = e[k*2].sum + e[k*2 + 1].sum;
    }
    

    最后再次膜拜dalao

    markdown真好玩
    我去我竟然写了一天

  • 相关阅读:
    Android数据库升级,数据不丢失解决方案
    Android项目中单实例数据库类,解决database is locked
    Android彩蛋效果,微信彩蛋效果
    Android性能优化
    Unable to execute dex: method ID not in [0, 0xffff]: 65536
    Android下载速度计算
    Android中不混淆类中函数
    Android中Parcelable接口用法
    开启Ubuntu Linux下VirtualBox访问USB功能
    touch移动触屏滑动事件
  • 原文地址:https://www.cnblogs.com/EiffelA-blog/p/13790506.html
Copyright © 2011-2022 走看看