Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

二分探索

代数的構造

seg_libでは抽象化セグメント木を実装するためにいくつかのトレイトを定義しています。 この章ではこれらのトレイトが表現する代数的構造とその背景について解説します。 ただし、数学的に厳密な議論は行いません(できない)。

モノイド

定義(モノイド)

を集合、を二項演算とする。

  • ( 結合法則 )
  • (単位元の存在)

上記の2つが成り立つとき、組をモノイドという。

セグメント木にはモノイドが乗りますが、これは十分条件です。 セグメント木には半群も乗ります。

定義(半群)

を集合、を二項演算とする。

  • ( 結合法則 )

上記が成り立つとき、組を半群という。

セグメント木に半群を乗せる場合、未初期化のノードは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]>>,
}
}

区間幅の利用

モノイド作用に区間幅の情報を利用したいことがあります。 これは取得クエリに区間幅の情報を付加する()ことで実現できますが、 不変量を何度も計算し直すことになり非効率です。 初期化時にで計算し、計算済みの値を取得すると良いです。

定義済み演算

Warning

この章は書きかけです

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()の挙動に影響を与えます。 最適な実装は型に依るため、ユーザーに定義してもらうことにしました。

Tip

i32Monoidトレイトを直接実装してはいけません。 例えば、(i32, +, 0)(i32, *, 1)が共存できないためです。

モノイド作用について

現在の実装では関連型にモノイドのテンプレートを利用することはできますが、act()はユーザーが定義することになっています。

Note

将来的には定義済み演算の間のモノイド作用を全て提供することも考えています。 区間幅の情報が必要な場合にusizeから適切に型を変換する必要があり、num_traitsFromPrimitiveが利用できます。

動的セグメント木

動的セグメント木は必要なノードだけを作成することで、巨大()な配列でもセグメント木を構築できるようにしたものです。

実装方針

高速化のために以下の方針を採用します:

  • ポインターではなくアリーナ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はスタックの長さがで抑えられることを利用して、アロケーションコストを節約するためにあります。

計算量

区間幅をとし、ある時点での更新クエリの数をとします。

arenarange querypoint 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_libcrates.ioで公開しているため、返り値にOption型を取ることにしました。 競プロなどではpanic!()するのもありです。

Tip

単位元以外の値で初期化することもできます。

可換モノイドの場合は簡単で、以下のようにしてモノイドに更新済みノード数を持たせるだけです。 初期値をもつノードの数は区間幅 - 更新済みノード数で与えられるので、繰り返し自乗法などをもちいてで計算できます。

#![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>>では0Noneと扱う最適化が施されており、追加のメモリは必要ありません。 arenaの0要素目は根なので、子のインデックスは常に正です。

更新クエリ

range update

あるノードに対応する区間で2つめのノードが更新される場合、新たにノードを作成する必要があります。 このとき、左の子のインデックス < 親のインデックス < 右の子のインデックスの関係を保持する必要があります。 また、途中結果を帰りがけ順序で再計算する必要があります。 ここで、reusable_stackを活用してアロケーションコストを節約します。

取得クエリ

動的セグメント木においてもっとも複雑な部分です。

観察

区間クエリが1つのノードに収まる場合と、2つのノードにまたがる場合で場合分けができます。

range query

タイプ1の場合、次に訪問するノードが右(左)の子ならば、合成順序は最後のノードよりも左(右)側です。 また、訪問順序が早いノードほど外側にあることが分かります。 reusable_stackを活用することで、この順序を再現できます。

タイプ2の場合に左の子を祖先に持つノード群を合成することを考えます。 L1から始めて、次に外側のL3に進むので、内側にあるL2は答えに含まれます。 L1自身の持つノードは外側から合成します。 L3の次は内側のノードであるL4に進みます。 L4の配下は明示されていないので何とも言えませんが、L3のノードを合成する場合は一番外側からであることは保証されます。 したがって、L3の持つノードへのポインター2reusable_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に格納しています。


  1. 配列上にノードを格納し、インデックスをポインター代わりにするテクニックのこと。キャッシュ効率が良く、所有権の管理が容易。

  2. ノードのインデックスと区別するために、アリーナ上のインデックスをポインターと呼ぶことにします。

参考文献

解説記事

本・資料

ライブラリ