2018/09/09 実装を少し変更しました
2021/02/18 数式を tex を使ったものに変更しました
2021/02/18 不適切な表現を変更しました
2021/02/18 msb の意味が適切でなかったので、修正しました
前半:
https://discuss.codechef.com/questions/117696/tutorial-disjoint-sparse-table
これの日本語解説
後半:
ポエム
Disjoint Sparse Table とは
Disjoint Sparse Table は静的な半群列の区間和を高速に計算できるデータ構造です。具体的には要素列長を としたとき、構築
/クエリ
を達成します。半群は演算が閉じていて結合則を満たせばいいので該当するものは多く、単純な和や積、
、
、
などが該当します。
※都合のため、クエリは閉区間で表現します。
お気持ち
基本的には累積和の考え方を使います。まず、中央から左右の方向に累積和を取ることを考えてみます。(下図参照)
それぞれのクエリに対して、右と左から対応する累積和を出してきて足します。こうすると、中央の境目を跨いでいる区間クエリについては 時間で計算できることが分かります。
データの持ち方と構築
問題は境目を跨いでいないクエリが計算できないことです。そこでこの「中央を跨ぐクエリを計算できる構造」をセグ木と同じ形状に乗せてみることにします。(下図参照)
見づらいかもしれませんが、セグ木のノードに対応する区間の累積和を乗せています。クエリ の例を見てください。
-
の境界を跨いでいないため一番上の段では処理が出来ませんが、
-
を跨いでいるため上から
段目右の部分で処理が可能です。他のクエリの例も試してみて欲しいのですが、必ず何れかの段で計算可能であることが分かると思います。この構造は二次元配列に素直に累積和を取って行けばよいため
で構築が可能です。
クエリ
次に問題となるのはクエリの時に、どの段を使用すればいいかを高速に判定する部分です。上の段から境界を跨いでいるかチェックしていては 時間掛かってしまうことが分かると思います。そこでbit演算を使います。具体的には閉区間で与えられたクエリの右端、左端のインデックスの
を取り、その最も左の
の位置 (most significant set bit) を計算します。
クエリ を例にすると,
となります。一番左の
が使用すべき段を示しています。この場合は左から
番目なので使用するのは上から
段目になります。
クエリ ではどうでしょうか。
となり一番左の
は左から
番目にあります。よって使用するのは上から
段目=最上段になります。
これはそれぞれの境界を跨ぐときに対応する桁がインクリメントされることから分かります。例えば -
の境界を通ると
の位が
から
になるため
を取ったときに
になります。
これで使用すべき段を 時間で判定し、
時間でのクエリ処理が可能になりました。嬉しい。
ポエム(読み飛ばし推奨)
半群というと近い構造にモノイドがあります。モノイドと言えばセグ木ですが、セグ木は半群でも扱うことができます。ここで時々聞く話として「セグ木は半群でもOKなら、半群を扱えるデータ構造として認識するべきではないか(そのほうが前提条件が緩くなるため)」という問題提起があります。
しかし私はセグ木は飽くまでも「モノイドを扱うデータ構造」であると主張したいです。これは書いたことがある人ならばわかると思うのですが、非再帰セグ木はモノイドでないとコードがとても冗長になります。単位元を仮定しないことによって場合分けが増えるのですが、この場合分けというのは半群にbool値を付けてモノイドに拡張しているのと本質的にはなんら変わらないのです。一方で Disjoint Sparse Table は単位元を仮定しなくても場合分けは増えません。ですから、半群は必ずモノイドに拡張できる*1ことも合わせてセグ木はモノイドを扱うとした方が美しいと思うのです。
実装するときの諸々
・長さ のクエリだけは右と左を足すことでは表現できないため、別処理をしないと駄目です*2
・セグ木のように 冪に拡張した方が楽ですが、拡張しなくても書けます
・most significant set bit の計算は bit 演算で で行える他、テーブルを事前計算して
にする方法や、__builtin_clz などの組み込み関数を用いる方法があります。
実装例(2018/5/12 追加)
累積和の計算の順番の都合のため、右半分のテーブルを逆転させて格納しています。それに伴い、やや分かりにくいコードになっているかも知れません。
面白いデータ構造募集中です