| ||
这个锯树结构其实并不是数据结构,只是一个 set 的用法二一。
首先是基本的思想,实现老司机树,只需要一个 `std::set` 以及熟悉 `std::set::upper_bound` 和一些其他特性。 但是需要注意的地方包括迭代器失效。
老司机树的核心思想、范围查询的核心思想 |
对范围(区间)进行有序保存;过程中对区间进行分割,分割后顺序删除原来的零碎区间;最后合并一个大区间。 |
下面列出一些需要注意的 C++ 要点。第一个要点是 std::set 的性能问题。一般 std::set 可能会用作顺序容器使用(虽然顺序访问的复杂度很危险,但是对于查找具有 logn 时间非常好用,也就是,偶尔的内容修改)。 `std::set` 的参与排序是通过 `operator <` 就能提供支持了。
C++ 中如果想修改 std::set 中的 元素而不用删除他重新插入(删除的 rebalance 费时间)要怎么做? |
使用 mutable 属性,只要不是 set 中参与排序的数据即可。 |
然后是怎么进行区间的分裂。我们先复习各种二分查找先吧。
二分查找的 lower_bound 和 upper_bound 中,如何查找以下情况:(1)第一个大于,(2)最后一个小于等于,(3)第一个等于。等等。。。 |
注意,ODT 树种用的是那种,要看具体情况具体分析。这里考虑如果最后一个小于等于。我们用 `upper_bound -1` 来实现这个需求。
对于需要合并一个 (l, r) 的区间,
- 首先,得到 (l+k, ...) k大于等于0 的区间迭代器,和 (r+k, ...) k 大于等于0 的区间迭代器
- 然后删除所有两个迭代器直接的节点,然后插入一个(l,r) 的节点。
然后补充一下对于最后一个小于的情况,如果不包含 r,如何进行分裂呢?
我们考虑普通的增量做法,目前已经有:
[1, 5] 和 [10, 16]
此时尝试分裂 6,upper_bound-1 得到的是 [1,5] 这个区间,此时分裂实际会删除 [1,5] 插入一个新的 [1, 6] 节点 和 [6, 5] 节点。
这是不可行的。因此我们只能用全量的做法!
如果存在 [1,5] 和 [10,16], 那么我们整体必须有(如果 boundary 是 20 的话):
([1, 5]: true), ([6, 9]: false), ([10, 16]: true), ([17, 20]: false)
这样的排列,从而保证查找 6 进行分裂的时候,会直接返回 6,因为 6 必定存在。如果 7 不存在,那么可能是这种情况:
([1, 7]: true), ([8, 9]: false), ([10, 16]: true), ([17, 20]: false)
即 6 必然被前一个包括了。
剩下一个查询没有说明,读写都可以 split 套模板,但读操作确实优化,比如例子,查找 【1,6】,只需读,6 性质和 【5,7】同样的,不用分裂:
([1, 2]: true), ([3, 4]: false), ([5, 7]: true), ([8, 20]: false)
总结修改访问的套路模板:
void performance(int l, int r) { auto itr = split(r + 1), itl = split(l); for (; itl != itr; ++itl) { // Perform Operations here } } |
注:珂朵莉树在进行求取区间左右端点操作时,必须先 split 右端点,再 split 左端点。若先 split 左端点,返回的迭代器可能在 split 右端点的时候失效,可能会导致 RE。
set 的迭代器失效问题 |
实际 set 的迭代器不会失效,除非你删除了他。 |
总结一下 Chtholly Tree 的思路:
- 所有的区间无论 true false 还是什么性质,都要整体存在,因此一开始是整个 domain 都是相同的性质。
- 每次对区间的修改,都会造成分裂。
- 区间操作先 split 右边再 split 左边。