问题引入:售票系统问题
假设在一个火车线路上有五个车站,它们分别在A、B、C、D、E五座城市(因此区间数 )。售票部门出售所能的车票,即起始站和终点站是任意的。由于城市之间距离较长,售票部分希望每位乘客都有座位,设总座位数是 。在处理新的订单请求时,机智的程序员怎么最快地判断是否还有余座呢?
拍脑袋给出的算法
开一个 维的数组,分别存储A-B, B-C, C-D, D-E段的乘客数,每当新订单出现时,就更新相应的区间,只要 个数都小于乘客数 (等价地, 个数中最大数小于乘客数 ), 就安排乘客上这趟车,否则告诉乘客余票不足。
每新添加一个订单,需要重新找 个数中的最大数,因此计算复杂度是 。这是最好的算法吗?
问题的分析
- 售票系统是一个在线的系统,会有源源不断的新数据(新订单)涌入,因此保留历史的中间变量可能会加速算法。
- 车站具有顺序性。其次,一个顾客的乘坐区间只能是连续的,即他不可能在A站上火车,B站下火车,又在D站上同一趟火车,最后在E站下了车。
线段树长神马样子?
仍然从售票系统例子出发。一开始售票数为0时,线段树长这样(图1):
基本特点容易看出:这是一个二叉树,在每个节点处,一个区间被分成了左、右两个区间。在每个节点和叶子处,都有一个伴随数字,这个数字代表这个区间的乘车人数(这么说不严谨,具体怎么更新,请大家耐着性子再往后看2333)。
首先打北边来了个小饶同学,他的乘车区间是B-C, B-C就是叶节点,线段树更新为(图2):
然后打南边来了个小鸣同学,他的乘车区间是B-D, 注意B-D=B-C+C-D, 而B-C和C-D是叶节点,线段树更新为(图3):
一直到现在都十分正常,和拍脑袋给出的算法一样。
这时急匆匆来了个小迪同学,他的乘车区间是A-D, A-D=A-C+C-D, C-D是叶节点这个没问题,但A-C是一个中间节点,现在有两种看法:
- 继续往下拆,A-C=A-B+B-C, 更新A-B 和B-C区间
- 保持整体, 更新A-C区间,线段树的哲♂学要求我们选择这种更新策略,如图4所示。
最后来了个小承同学,他买了张全程票,即A-E, 注意到A-E恰好就是根,于是不必往下拆了,更新A-E即可,如图5所示。按照拍脑袋给出的算法,等价的结果应该如图5′ 所示。
是骡子是马,拉出来溜溜了。我们需要知道区间的最多人数,从叶节点出发,我们可以算出所有区间人数的最大值(因为有 )。如下图所示:
两种办法求出的最大值都是4,可以说明线段树和拍脑袋方法的等价性。好了,线段树学习结束了。exm???那线段树优势在哪呢?
为什么线段树?
更新(添加新订单)
- 对于拍脑袋的办法,需要 的运算量
- 对于线段树,只需要 的运算量,严格的上界是
- 简单地理解,当一个区间恰好等于一个节点(而不必是叶节点),更新即可完成
查询(计算所有区间的最多乘客数)
- 对于拍脑袋的办法,采用打擂法求最大值,需要 的运算量
- 对于线段树,利用了树的结构,还是需要 的计算量,而且在中间节点处要记得加上相应的数(专业名称:lazy tag等等)。
所以线段树的优势,主要还是体现在在线(online)的问题上。
一般的线段树
博客:这里有严格定义和相应的实现代码
【数据结构系列】线段树(Segment Tree) www.cnblogs.com
维基:这里是一般的线段树定义