[Python]线段树与二分法

一、先从"二分查找"说起

假设你有一本按页码编号的电话簿,从第1页到第100页。你想找第37页上的人名。

普通查找:从第1页开始翻,一页一页往后翻,翻到第37页。最坏情况要翻100次。

二分查找

  1. 先翻到中间第50页。37 < 50,所以目标在前半本。

  2. 再翻到前半本的中间第25页。37 > 25,所以在后半本(25~50之间)。

  3. 再翻到25~50的中间第37页。找到了。

只翻了3次。这就是二分------每次把搜索范围砍掉一半


二、从"二分查找"到"树"

现在问题变了:你不再只是找某一页,而是要频繁地查任意连续几页里,人名拼音最靠后的是谁(相当于最大值)。

比如问你:"第30页到第50页之间,人名拼音最靠后的是谁?"

如果你每次都从第30页翻到第50页,一页一页比过去,那就太慢了。

能不能像二分查找那样,提前把一些"汇总信息"存好,查的时候直接拿?

这就是"树"要做的事情。


三、画一棵"汇总树"

假设我们有8个时间戳:1, 2, 3, 4, 5, 6, 7, 8。每个时间戳上有一个数值。

我们先把它们两两分组,每组选出一个最大值:

复制代码
第1层(原始数据):
时间戳:  1    2    3    4    5    6    7    8
数值:    10   5    8    3    12   7    9    4

第2层(两两一组取最大值):
(1,2) → max(10,5) = 10
(3,4) → max(8,3) = 8
(5,6) → max(12,7) = 12
(7,8) → max(9,4) = 9

第3层(再两两一组取最大值):
(1~4) → max(10,8) = 10
(5~8) → max(12,9) = 12

第4层(最终):
(1~8) → max(10,12) = 12

把这个过程画成树的样子:

复制代码
(1~8): 12
                /         \
         (1~4): 10       (5~8): 12
         /      \         /      \
   (1~2): 10  (3~4): 8  (5~6): 12 (7~8): 9
    /    \     /    \    /    \     /    \
   1:10  2:5  3:8  4:3  5:12 6:7  7:9  8:4

这棵树就是线段树。

  • 最下面一排是原始数据(叶子节点)。

  • 上面每一层都是下一层的"汇总"------两个子节点的最大值。

  • 最上面是整棵树的"总最大值"。


四、用这棵树来查询

现在问你:"时间戳3到时间戳6之间的最大值是多少?"

不用一个一个看。我们从树顶往下走:

  1. 根节点 (1~8):太大了,不完全在3~6内,所以往下走。

  2. 左孩子 (1~4):部分重叠(3~4在范围内),继续往下。

  3. 右孩子 (5~8):部分重叠(5~6在范围内),继续往下。

  4. (1~4) 的左孩子 (1~2):完全不重叠(1~2不在3~6内),跳过。

  5. (1~4) 的右孩子 (3~4):完全在3~6内,直接拿它的值 = 8。

  6. (5~8) 的左孩子 (5~6):完全在3~6内,直接拿它的值 = 12。

  7. (5~8) 的右孩子 (7~8):完全不重叠,跳过。

结果:max(8, 12) = 12。

我们只看了几个节点,就得到了答案,不需要遍历所有8个时间戳。


五、为什么叫"动态开点"?

刚才的例子中,我们把所有8个时间戳都画出来了。但实际题目中,时间戳范围是1到10^9,你不可能真的把这10亿个节点都建好------内存不够。

动态开点 的意思是:只有当你真正用到某个时间戳时,才创建它对应的节点。

比如你只收到了时间戳1、5、10的数据,那树就只长成这样:

复制代码
(1~10^9)
               /         \
         (1~5 * 10^8)   (5 * 10^8+1 ~ 10^9)
         /      \
    (1~2.5 * 10^8) (2.5 * 10^8+1 ~ 5 * 10^8)
    /    \
 (1~1)  (2~2.5 * 10^8)
  |
 1:10

实际上,树只会沿着你用到的路径生长。没用到的区间,节点根本就不存在(None)。这就叫"动态开点"------用多少,长多少


六、总结:线段树到底干了什么?

你的问题 线段树的回答
我要在时间戳5上存一个值 从树顶往下走,找到时间戳5的叶子,存进去。然后一路回溯,更新沿途所有父节点的最大值。
我要查时间戳3~6的最大值 从树顶往下走,碰到完全在3~6内的节点就直接拿值,碰到完全不在的就跳过,部分重叠的就继续往下。最后把所有拿到的值取最大。
时间戳范围太大 动态开点,只用到的节点才创建,不浪费内存。

整个过程就是"二分思想"的延伸:把一个大区间不断对半拆分,每个小区间都提前算好"汇总值"。查询时,通过二分快速定位到需要的小区间,直接拿汇总值,不用一个一个数。


七、一个让你彻底理解的类比

想象你是一个班主任,手下有1000个学生,学号从1到1000。你想随时知道任意连续学号段里,最高分是多少

笨办法:每次有人问,你就把那个学号段的所有学生成绩翻一遍。1000个学生问1000次,就是100万次操作。

线段树办法:你提前把所有学生按学号分组:

  • 1~1000 为一组,记下最高分

  • 分成两组:1~500 和 501~1000,分别记下最高分

  • 再分:1~250、251~500、501~750、751~1000,分别记下最高分

  • ......

  • 直到每个学生单独一组

现在有人问"学号237到学号684的最高分是多少",你只需要:

  1. 看237~684覆盖了哪些"提前分好组"的区间

  2. 把这些区间的最高分拿出来比一比

  3. 得出答案

你不需要看每一个学生的成绩,只需要看几个"小组汇总"就够了。

这就是线段树。

相关推荐
ZhangShao06071 小时前
题解:CF2249B
c++·算法
Tisfy3 小时前
LeetCode 3345.最小可整除数位乘积 I:暴力枚举(从n开始尝试)
数学·算法·leetcode·题解·枚举
DolphinDB智臾科技3 小时前
告别 TB 级数据搬运:DolphinDB 滤波算法库,重塑物联网数据价值链
物联网·算法
可编程芯片开发4 小时前
基于双PI控制器结构的六步逆变器供电无刷直流电机调速simulink仿真
算法
Sagittarius_A*5 小时前
后量子密码|通识认知 02|量子威胁核心原理:Shor 算法如何击穿传统公钥密码
算法·信息安全·密码学·量子计算
青山木5 小时前
Hot 100 --- 在排序数组中查找元素的第一个和最后一个位置
java·数据结构·算法·leetcode
其美杰布-富贵-李5 小时前
聚类算法详解:K-means、层次聚类与 DBSCAN
算法·机器学习·kmeans·聚类
碧海银沙音频科技研究院6 小时前
4 BES2710编译环境搭建方法
嵌入式硬件·算法
mifengxing6 小时前
LeetCode 238 除自身以外数组的乘积|无除法O(n)时间+O(1)空间双解法
java·算法·leetcode