セグメント木

概要

区間に対する集約処理をするときによく使われる。
$a_1,a_2,...,a_n$ の配列に対し、以下を処理する。(一点更新・区間取得)

  • $update(i,x)$ : $a_i$ を $x$ で更新する
  • $query(l,r)$ : 区間 $[l,r) = a_l,a_{l+1},...,a_{r-1}$ の集約値を求める

「集約」には、身近な例では総和とか最小値とかがあてはまる。

管理できるものの条件

どんな「$a_i$ の型」に対して、どんな「集約操作」を行うか、という(型, 操作)の組を、セグメント木は柔軟に変えられる。

たとえば前者が『整数全体の集合』なら、演算はたとえば以下のことが可能である。

  • 区間和、区間積
  • MIN, MAX
  • 最小公倍数LCM、最大公約数GCD
  • ビット演算 AND、OR、XOR

より厳密に定義しようとすると多少抽象的な話になる。

モノイド であればセグメント木で管理することができるとされている。 モノイドとは、例えば「整数全体」と「足し算」のように、ある要素の集合 $S$ とその上での二項演算 $\bullet$ の組合せ $(S, \bullet)$ の内、以下の条件を満たすものである。

  • 結合律が成り立つ=計算の順序を入れ替えてもいい
    • $(a \bullet b) \bullet c = a \bullet (b \bullet c)$
  • 単位元が存在する
    • $S$ のどんな要素 $a$ に対しても影響を及ぼさない要素 $e$ が存在する
    • $a \bullet e = e \bullet a = a$

聞き馴染みが無くとも、足し算とかかけ算とか、割と身近なものでこの性質を満たすものは多い。

結合律が必要な理由については、セグメント木は以下のような形でまとまった区間を前計算して、必要な部分を掻い摘まんで集約するので、

|        1*2*3*4*5*6*7*8        |
|    1*2*3*4    |    5*6*7*8    |
|  1*2  |  3*4  |  5*6  |  7*8  |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

↓ 2~6の集約値を求めるのに参照する箇所

|                               |
|               |               |    2 * (3*4) * (5*6)
|       |  3*4  |  5*6  |       |
|   | 2 |   |   |   |   |   |   |

ここでもし * の演算が結合率を満たさず $(((2*3)*4)*5)*6 \neq 2*(3*4)*(5*6)$ だと困ることが分かる。
結合律が成り立たない例としては、実数と冪乗が挙げられる。$\displaystyle (4^3)^2=4096, 4^{(3^2)}=262144$

単位元が存在しない例としては、自然数と足し算があげられる。(足し算は単位元が0だが、自然数に0は含まれない)
単位元の存在は初期値を埋めておくための実装上の問題。

だが、実は単位元は $S$ をちょっと拡張することで無理矢理作ることも出来て、 たとえばタプルにして「自身は単位元であるフラグ」を付け、それがTrueのものとの演算は常にもう片方を返すようにする、などで単位元と振る舞い的に変わらなくなる。
問題を解く上で絶対に $S$ はこの型でないといけない、なんてことは普通無いので、単位元はあまり気にしなくてよい。

バリエーション

構造が比較的単純なため、バリエーションもいくつかある。

  • 一点更新・区間取得(基本)
  • 区間更新・一点取得(双対セグメント木)
  • 区間更新・区間取得(遅延セグメント木)
  • 多次元化
  • 永続化
    • 更新を加えたときに、後から遡って「時刻 $t$ での状態」を復元できる構造

双対セグメント木

区間更新・一点取得を行えるデータ構造。できることが通常のセグメント木と逆。

通常のセグメント木は、「更新は、一番下から上に遡上して状態を更新」「取得は、なるべく広い区間をまとめて取得」

【通常のセグメント木】
|       +       |    [更新]
|   +   |       |    +: i=3 を更新する際に更新が必要なノード
|   | + |   |   |
| | | |+| | | | |       一番下のノードから、真上のノードを順次更新
 0 1 2 3 4 5 6 7

|               |    [取得]
|       |   *   |    *: [1, 8)の集約値を取得するのに参照するノード
|   | * |   |   |
| |*| | | | | | |       2~3, 4~7 の部分は末端を参照しなくてよい
 0 1 2 3 4 5 6 7

双対は、操作も逆。

【双対セグメント木】
|               |    [更新]
|       |   +   |    +: [1, 8) を更新するときに更新するノード
|   | + |   |   |
| |+| | | | | | |       2~3, 4~7 の部分は末端を更新しなくてよい
 0 1 2 3 4 5 6 7

|       *       |    [取得]
|   *   |       |    *: i=3 を取得する際に参照するノード
|   | * |   |   |
| | | |*| | | | |       一番下のノードから、真上のノードに溜まった更新値を合成していく

非可換の場合

可換性 とは、操作の順番を入れ替えても答えが変わらない性質。足し算やかけ算、MINやMAXなど、これが成り立つものは多い。

  • $(a+b)+c=(a+c)+b$

モノイドの要件には入っていないので、セグ木に載せる上で必ずしも成り立っている必要は無い。
ただしもし非可換なら、通常のセグ木の場合、演算の左と右に来るものを取っ替えないように注意する必要がある。

通常のセグ木では注意するくらいで済むが、双対セグ木では、必要な実装がかなり変わってくる。
具体的に言うと、更新時、更新したいノードより上に溜まっているものがあれば、下まで押し込む必要がある。1)

以下の例を考える。演算として + を使っているが、今はこれが可換性が成り立たない演算とする。

|               |    [更新]
|   5   |       |    [0, 4) に 5 が足された
|   | 3 |   |   |    [3, 4) に 1 が足された
| | | |1| | | | |    [2, 4) に 3 が足された
 0 1 2 3 4 5 6 7

この時、取得するときもこの更新順に合成しなければならない。たとえば $i=3$ に対しては $5+1+3$ としなければならない。
しかし、このまま下から合成すると $1+3+5$ となり、間違った答えとなる。

ここで、$5$ が足された後、より狭い範囲に対して更新操作があったら下まで押し込むようにすると、

|               |    [更新]
|   5   |       |
|   |   |   |   |    [3, 4) に 1 を足したい
| | | | | | | | |
 0 1 2 3 4 5 6 7

|               |
|       |       |    押し込む
| 5 | 5 |   |   |
| | | | | | | | |

|               |
|       |       |    押し込む
| 5 |   |   |   |
| | |5|5| | | | |

|               |    更新したいノードより上のノードに
|       |       |    溜まっている値が無くなったところで、
| 5 |   |   |   |    改めて [3, 4) に 1 を足す
| | |5|6| | | | |

|               |    
|       |       |    [2, 4) に 3 を足す
| 5 | 3 |   |   |    下の方に溜まっている値があるのは気にしなくてよい
| | |5|6| | | | |
 0 1 2 3 4 5 6 7

これで、$i=3$ を取得するときは下から順に合成していけばよくなる。
$6+3$ となるが、$6$ は $5+1$ をこの順に演算した結果なのでこれは $(5+1)+3$ であり、正しく求められている。

具体的にどこを押し込むか。
更新が必要なノードのうち、左端と右端の、真上一直線上のノードに溜まっている値は上から順に押し込むとよい。最大 $2 \log{N}$ 個。

|               v               |
|       v       |       v       |    [4, 13) を更新
|       |   u   |   u   |   v   |    u: 更新が必要なノード
|   |   |   |   |   |   | v |   |    v: 押し込みが必要なノード(上から順に)
| | | | | | | | | | | | |u| | | |
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5

区間の両端の真上一直線上のノードだけでいいのは、 「これより外のノードは、そもそも更新しないので関係ない」 「この間に完全に含まれるノードは、必ず一番上のノードで受け止められ、下のノードは更新されないので押し込む必要は無い」と思えばよい。

遅延評価セグメント木

区間更新・区間取得ができる。上位互換だが、まぁ実装重くなるしバグも埋め込みやすくなるので、過不足の無いデータ構造を選ぶ。

(番外) Fenwick木

セグメント木を、機能を限定的にして実装を軽くしたものの1つに、Fenwick木(Binary Indexed Tree)がある。

indexの表現方法

セグメント木には、2つのindex(っぽい概念)が登場する。

  • (1) updateやqueryで与えられる $a_i$ の $i$ を指すindex
  • (2) 二分木を1つの配列で表現する実装において、その配列上のindex
      1
     /  \
   2    3     ←(2)
  / \    / \
4  5  6  7
||  ||  ||  ||    対応
a1  a2  a3  a4  ←(1)

indexは、0始まりか1始まりかを意識する必要がある。

プログラム全般的には、多くの言語で配列の添字が0-indexなのでそれに倣うことが多いが、 特に(2)の二分木の実装では、1-indexにした方が都合が良い。

2進数にしたとき、深さと桁数が一致する。また、子のindexを右bitshiftすると親になるので、親子を辿るindexの計算が簡潔になる。

        0001
       /   \
    0010   0011
    / \     / \
0100 0101 0110 0111

実装例

一点更新・区間取得

基本

  • 2019/11 get_min() を、indexとbit演算を使った書き方に変更(速度up)

整数と区間最小値

再帰を使った書き方

書き方がシンプルになり人によってはアレンジしやすくなる一方、Python、PyPyでは相対的に遅くなるため非推奨。

整数と区間最小値

モノイドを注入する

(単位元、演算)を外部から与えられるようにして柔軟性を持たせた形。ただし実行速度は多少悪くなる。

モノイドは可換でなくてもOK(のはず)。

Python3

1)
この操作は遅延評価セグメント木にも出てくる
programming_algorithm/data_structure/segment_tree.txt · 最終更新: 2020/12/24 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0