二分探索
代数的構造
seg_lib
では抽象化セグメント木を実装するためにいくつかのトレイトを定義しています。
この章ではこれらのトレイトが表現する代数的構造とその背景について解説します。
ただし、数学的に厳密な議論は行いません(できない)。
モノイド
セグメント木にはモノイドが乗りますが、これは十分条件です。 セグメント木には半群も乗ります。
セグメント木に半群を乗せる場合、未初期化のノードはNone
で表現することになります。
すなわち、ノードにはOption<S>
が格納されます。
Option<S>
上の二項演算を以下で定義すると、半群をモノイドに拡張できます。
#![allow(unused)] fn main() { fn binary_operation<S>(lhs: Option<S>, rhs: Option<S>) -> Option<S> { match (lhs, rhs) { (Some(lhs), Some(rhs)) => todo!("lhs ⋅ rhs"), (Some(lhs), None) => Some(lhs), (None, Some(rhs)) => Some(rhs), (None, None) => None } } }
このようにモノイドと半群の区別はあまり重要ではありません。
実装上単位元があると便利なので、seg_lib
ではモノイドを採用しました。
#![allow(unused)] fn main() { pub trait Monoid { /// The set of the monoid. type Set; /// If [`Self::combine`] is commutative, some operations can be optimized. /// /// If unsure about the commutativity, use [`false`] for safety. /// /// # Commutative low /// /// ```text /// a · b = b · a ∀ a, b ∈ Set /// ``` const IS_COMMUTATIVE: bool; /// Returns the identity element. fn identity() -> Self::Set; /// Combines the two elements and returns the result. /// /// # Warning /// /// If the operation is **not** commutative, the position of the arguments matters. fn combine(lhs_or_prev: &Self::Set, rhs_or_new: &Self::Set) -> Self::Set; } }
Set
をジェネリック型ではなく関連型としてもっています。
こうすることで、ジェネリックパラメーターが1つ減り、実装がシンプルになります。
#![allow(unused)] fn main() { pub struct SegmentTree<Query> where Query: Monoid, { /// Use `Box<T>` because the length is significant as follows. /// /// - data\[0\] : dummy node (meaningless) /// - data\[1..n\] : nodes to store the combined value of the children. /// - data\[n..2n\]: nodes to store value for each cell. data: Box<[<Query as Monoid>::Set]>, /// `len` (number of elements) and offset (dummy + cache) len_or_offset: usize, // /// This and its ancestors are all invalid (cached result may be broken) // min_invalid_node: usize, } }
おまけ
セグメント木には半群ですらない代数的構造も乗ります。 問題にしている範囲で半群のように振舞えば何でもよいのです。
あるデータ列について、隣り合う要素の間に結合的な二項演算が定義されているとき、これはセグメント木に乗る。 とくに、隣り合わないノードの間に演算が定義されていなくともよい。
モノイド作用
遅延伝搬セグメント木ではいくつかの要素をまとめて更新します。 取得クエリに対応するモノイドを、更新クエリに対応するモノイドをとかきます。 モノイド作用は2つのモノイドをつなぐ糊のようなものです。 遅延伝搬セグメント木が正しく動作するためには「いくつかの要素をまとめて更新する」ことで「各要素をそれぞれ更新した結果」を再現できなければなりません。
#![allow(unused)] fn main() { pub trait MonoidAction { /// The set of the monoid for update operation. type Map: Monoid; /// The set of the monoid for query operation. type Set: Monoid; /// Set [`true`] to use the segment size in [`Self::act()`] const USE_SEGMENT_SIZE: bool; /// Acts the mapping on the element and returns the result. /// /// # Size dependency /// /// You can access segment size if you want. /// This is equivalent to attaching the segment size information to [`Self::Set`] but more performant. fn act( mapping: &<Self::Map as Monoid>::Set, element: &<Self::Set as Monoid>::Set, size: Option<usize>, ) -> <Self::Set as Monoid>::Set; } }
Map
は更新クエリ、Set
は取得クエリに対応します。
#![allow(unused)] fn main() { pub struct LazySegmentTree<Action> where Action: MonoidAction, { data: Box<[<<Action as MonoidAction>::Set as Monoid>::Set]>, lazy: Box<[<<Action as MonoidAction>::Map as Monoid>::Set]>, /// calculate if [`MonoidAction::USE_SEGMENT_SIZE`] is `true`. segment_size: Option<Box<[usize]>>, } }
区間幅の利用
モノイド作用に区間幅の情報を利用したいことがあります。 これは取得クエリに区間幅の情報を付加する()ことで実現できますが、 不変量を何度も計算し直すことになり非効率です。 初期化時にで計算し、計算済みの値を取得すると良いです。
定義済み演算
seg_lib
を利用するたびにMonoid
トレイトなどを実装するのは面倒です。
そこで、典型的なモノイドのテンプレートを用意しました。
一覧はドキュメントから確認できます。
モノイドのテンプレート
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Add<T>(PhantomData<T>); impl<T> Monoid for Add<T> where T: Zero, for<'a> &'a T: std::ops::Add<Output = T>, { type Set = T; const IS_COMMUTATIVE: bool = true; fn identity() -> Self::Set { T::zero() } fn combine(lhs_or_prev: &Self::Set, rhs_or_new: &Self::Set) -> Self::Set { lhs_or_prev + rhs_or_new } } }
例えば、Add<i32>
はモノイド(i32, +, 0)
に対応します。
トレイト境界のとり方には他にもいくつかのパターンが考えられます。
T: Copy + Zero + std::ops::Add<Output = T>
T: Clone + Zero + std::ops::Add<Output = T>
これらは、combine()
の挙動に影響を与えます。
最適な実装は型に依るため、ユーザーに定義してもらうことにしました。
モノイド作用について
現在の実装では関連型にモノイドのテンプレートを利用することはできますが、act()
はユーザーが定義することになっています。
将来的には定義済み演算の間のモノイド作用を全て提供することも考えています。
区間幅の情報が必要な場合にusize
から適切に型を変換する必要があり、num_traits
のFromPrimitive
が利用できます。
動的セグメント木
動的セグメント木は必要なノードだけを作成することで、巨大()な配列でもセグメント木を構築できるようにしたものです。
実装方針
高速化のために以下の方針を採用します:
- ポインターではなくアリーナ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
に格納しています。
-
配列上にノードを格納し、インデックスをポインター代わりにするテクニックのこと。キャッシュ効率が良く、所有権の管理が容易。 ↩
-
ノードのインデックスと区別するために、アリーナ上のインデックスをポインターと呼ぶことにします。 ↩