[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. 得出答案

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

这就是线段树。

相关推荐
青少儿编程课堂5 分钟前
图形化编程实战:智能交通灯调度台,一个作品讲透循环、条件与广播
c++·python·算法·bfs·信息学竞赛
evans在进步8 分钟前
LeetCode 438 找到字符串中所有字母异位词:滑动窗口与排序解法详解
算法·leetcode·职场和发展
M78佐菲12 分钟前
Linux多线程:创建、回收与互斥同步
linux·笔记·学习·算法
小手cool17 分钟前
使用递归对数组进行反转操作
java·数据结构·算法
兔兔兔兔128 分钟前
记录C++ 13
开发语言·c++·算法
码行山野赴时序归途33 分钟前
从暴力到最优:三道 C 语言入门题的解法思路
c语言·开发语言·数据结构·算法·leetcode·排序算法
土司大王34 分钟前
LeetCode hot100——随机链表的复制
算法·leetcode·链表
叠层归一研究院35 分钟前
螺旋时空:E框架的整合宇宙论
人工智能·经验分享·算法·机器学习·agi
磁场转动100万匹1 小时前
线性回归算法实现训练数据集、测试数据集的缺失值填充
算法·机器学习·线性回归
乐迪信息2 小时前
防爆AI摄像机实时监测船舶异常姿态
大数据·人工智能·算法·安全·目标跟踪