動的セグメント木
動的セグメント木は必要なノードだけを作成することで、巨大()な配列でもセグメント木を構築できるようにしたものです。
実装方針
高速化のために以下の方針を採用します:
- ポインターではなくアリーナ1で木をつくる
- 非再帰実装
また、簡単のために、追加の制約を課します:
Range<isize>
で初期化し、区間幅は固定- 単位元で初期化
#![allow(unused)] fn main() { pub struct DynamicSegmentTree<Query> where Query: Monoid, { arena: Vec<Node<<Query as Monoid>::Set>>, range: Range<isize>, // save allocation cost reusable_stack: Vec<usize>, } }
Node<T>
の詳細は後述します。
非可換モノイドでは要素の合成順が重要です。
後述のように、正しい順序で要素を合成するためにスタックを使用します。
reusable_stack
はスタックの長さがで抑えられることを利用して、アロケーションコストを節約するためにあります。
計算量
区間幅をとし、ある時点での更新クエリの数をとします。
arena | range query | point update | |
---|---|---|---|
time/space complexity |
実装例
初期化子
seg_lib
において動的セグメント木の区間は初期値で固定されます。
Range
型は不正な区間(1..0
など)を持つことができるため、これをケアする必要があります。
区間幅がゼロの場合もまとめてrange.is_empty()
で処理できます。
#![allow(unused)] fn main() { pub fn with_capacity(range: Range<isize>, q: usize) -> Option<Self> { if range.is_empty() { None } else { // never panic: `range.len()` is always larger than 0 let height = range.len().ilog2() as usize + 1; Some(Self { arena: Vec::with_capacity(q * height), reusable_stack: Vec::with_capacity(height * 4), range, }) } } }
seg_lib
はcrates.ioで公開しているため、返り値にOption
型を取ることにしました。
競プロなどではpanic!()
するのもありです。
単位元以外の値で初期化することもできます。
可換モノイドの場合は簡単で、以下のようにしてモノイドに更新済みノード数を持たせるだけです。
初期値をもつノードの数は区間幅 - 更新済みノード数
で与えられるので、繰り返し自乗法などをもちいてで計算できます。
#![allow(unused)] fn main() { use seg_lib::{DynamicSegmentTree, ops::Add}; let mut dst = DynamicSegmentTree::<(/* operation */, Add<usize>)>::new(/* range */); dst.point_update(/* index */, (/* value */, 1)); let (combined, n) = dst.range_query(/* sub range */); }
非可換モノイドを扱う場合、計算順序を守るために、実装に踏み入る必要があります。 ノードごとにかかるので、取得クエリ・更新クエリの計算量がに悪化します。 または、動的遅延伝搬セグメント木を利用してもよいです。
ノード
動的セグメント木では必要最小限のノードを作成することで、空間計算量を実現します。
通常のセグメント木のように、各ノードに対応する区間の途中結果をもつ場合、空間計算量がになってしまいます。 対応する区間に更新されたノードが1つしかない場合にノードの作成を遅延することで、空間計算量をに削減できます。
#![allow(unused)] fn main() { struct Node<T> { index: isize, element: T, /// may be `None` if `combined == element`, avoiding `clone()` combined: Option<T>, left_ptr: Option<NonZeroUsize>, right_ptr: Option<NonZeroUsize>, } }
seg_lib
の動的セグメント木では区間幅を固定しているので、各ノードが持つ区間情報は暗黙的に与えられます。 区間情報は根から子を辿る際に計算できるので、ノードには持ちません。- ノードに対応する区間の途中結果には
Option
型を採用しました。 これはトレイト境界<Query as Monoid>::Set: Clone
を回避するためです。 - 通常
Option
型は追加のフラグに8byte(64bitシステムの場合)利用しますが、Option<NonZero<T>>
では0
をNone
と扱う最適化が施されており、追加のメモリは必要ありません。arena
の0要素目は根なので、子のインデックスは常に正です。
更新クエリ
あるノードに対応する区間で2つめのノードが更新される場合、新たにノードを作成する必要があります。
このとき、左の子のインデックス < 親のインデックス < 右の子のインデックス
の関係を保持する必要があります。
また、途中結果を帰りがけ順序で再計算する必要があります。
ここで、reusable_stack
を活用してアロケーションコストを節約します。
取得クエリ
動的セグメント木においてもっとも複雑な部分です。
観察
区間クエリが1つのノードに収まる場合と、2つのノードにまたがる場合で場合分けができます。
タイプ1の場合、次に訪問するノードが右(左)の子ならば、合成順序は最後のノードよりも左(右)側です。
また、訪問順序が早いノードほど外側にあることが分かります。
reusable_stack
を活用することで、この順序を再現できます。
タイプ2の場合に左の子を祖先に持つノード群を合成することを考えます。
L1
から始めて、次に外側のL3
に進むので、内側にあるL2
は答えに含まれます。
L1
自身の持つノードは外側から合成します。
L3
の次は内側のノードであるL4
に進みます。
L4
の配下は明示されていないので何とも言えませんが、L3
のノードを合成する場合は一番外側からであることは保証されます。
したがって、L3
の持つノードへのポインター2をreusable_stack
に格納します。
右側の子孫たちの合成順序で確認してみてください。
R1
の次は内側に進むので、reusable_stack
に格納します。
R2
の次は外側に進むので、R3
を右から合成し、R2
を右から合成します。
reusable_stack
における左右の区別
ここまでで、reusable_stack
には0個以上のポインターが格納されています。
ポインターはただのusize
なので、左右の区別をするために工夫をする必要があります。
ポインターはアリーナのインデックスであり、アリーナはVec
です。
実は、Vec
の容量の最大値はisize::MAX
と決まっています。
たとえば、Vec::with_capacity()
でisize::MAX
より大きな容量を与えるとpanic
します。
したがって、usize
の最上位ビットをフラグとして利用できます。
#![allow(unused)] fn main() { while let Some(ptr) = self.reusable_stack.pop() { const MSB: usize = 1_usize.rotate_right(1); res = if ptr & MSB == 0 { <Query as Monoid>::combine(self.arena[ptr].get_element(), &res) } else { <Query as Monoid>::combine(&res, self.arena[!ptr].get_element()) } } }
res
はタイプ2の場合の内側の計算結果です。
左側のノードへのポインターはそのまま、右側のノードへのポインターはビット反転してから、reusable_stack
に格納しています。