競技プログラミングの文脈で出てくるセグメント木について「どう使うか」ではなく、「なぜ使えるのか」を考察します。
セグメント木の解説記事の多くは、配列を2べきの長さに区切って完全二分木に載せる様子や、任意の区間が高々 O(logn) 個のノードに分解できる様子を図で示すことに重点が置かれています。
図を見れば「なるほど、正しそうだ」という感覚は得られるのですが、その感覚がどこまで厳密な正しさの根拠になっているのか理解できていませんでした。
この記事では、そうしたギャップを埋めることを目標に、セグメント木というデータ構造を天下り的に構成したあとで、それが期待通りに動作すること、そして期待通りの計算量で動作することを、モノイドの結合則だけを用いて証明します。
なお、「どのようなクエリがセグメント木で処理できるか」というより一般的な問いについてはで体系的に議論されています。この記事はそれとは逆に、モノイドを載せるところから出発し、データ構造と代数構造の対応そのものに焦点を当てます。
おことわり
この記事は、セグメント木に一定の親しみがある私が、セグメント木の持つ優れた性質について証明を通して理解するために整理した内容です。
そのため、セグメント木を1から学習しようとしている方、セグメント木の実装を知りたい方、具体例を通じてセグメント木の使い方を知り、競技プログラミングで出題される問題を解きたい方向けの内容ではないことをご了承ください。
また、内容については確認したつもりではありますが、間違いを発見した方は教えて頂けると助かります。
前提
- セグメント木に関する基本的な知識を仮定します。
- モノイドなどの数学の概念を断りなく利用します。
- モノイドは可換性を仮定しません。
- コードを全て疑似コードで記述します。
- 配列・区間のインデックスは 0-indexed とします。
- 計算を範囲を考える際に区間を用いて説明しますが、ここでは 半開区間 を用います。
アプローチ
最初に木の構造そのものを定義し、次に具体的なデータ構造(セグメント木)を定義し、そのあとに構成したデータ構造がセグメント木として成り立って欲しい性質を満たすことを証明します。
木の構造
セグメント木の議論を始める前に、土台となる木の構造だけを先に定義しておきます。ここで導入する概念(区間 seg、高さ h、子ノードの記法、パス)はモノイドの値 vτ とは無関係で、純粋に組合せ論的な性質です。
定義
- 管理する配列の長さを n=2d とします。一般の n は単位元でパディングしてこの形に帰着させます。
- 深さ d の完全二分木 T を考え、各ノード τ∈T に区間 seg(τ)⊆[0,n) を対応させます。
- 根 τ0 は seg(τ0)=[0,n) とします。葉は単一のインデックス seg(τ)=[i,i+1),0≤i<n で表せるものです。
- 非葉ノード τ は必ず2つの子を持ちます。モノイドの非可換性のために子ノードの順序を明示する必要が頻繁に生じるので、左の子を lch(τ)、右の子を rch(τ) と書くことにします。子は親の区間を前半・後半に分割するもの、すなわち
seg(τ)=seg(lch(τ))⊔seg(rch(τ))
(インデックス順を保った非交和、すなわち seg(lch(τ)) の全要素は seg(rch(τ)) の全要素より小さい)として定義します。
- 高さ h(τ) を、葉について h(τ)=0、非葉ノード τ について
h(τ)=h(lch(τ))+1=h(rch(τ))+1
(完全二分木なので両辺は一致し、これも定義の一部とします)として定義します。根の高さは d であり、∣seg(τ)∣=2h(τ) が成り立ちます。特に、非葉ノード τ について
h(lch(τ))=h(rch(τ))=h(τ)−1
が成り立ちます。
以降、「非葉ノード τ の子を lch(τ),rch(τ) とする」という前提のもとで、上の2つの関係式を断りなく使います。
ノードから根へのパス
各インデックス i∈[0,n) に対して、i∈seg(τ) となるノード τ 全体を Pi と書き、これを(葉 i から根 τ0 への)パスと呼びます。
補題1 (パスの一意性)
任意の i∈[0,n) と高さ h∈{0,1,…,d} に対して、Pi に属する高さ h のノードはちょうど1つ存在する。
証明. h に関する下降帰納法(d から 0 へ)で示す。h=d のときは根 τ0 のみが高さ d であり、seg(τ0)=[0,n) より任意の i について τ0∈Pi。これが唯一の高さ d のノードなので成立。
高さ h+1 のノード τ について τ∈Pi であるものがちょうど1つ存在すると仮定する。seg(τ)=seg(lch(τ))⊔seg(rch(τ)) は非交和なので、i∈seg(τ) ならば i∈seg(lch(τ)) と i∈seg(rch(τ)) のちょうど一方のみが成り立つ。すなわち τ の子のうち Pi に属するものがちょうど1つ存在する。これが高さ h の唯一のノードであり(他の高さ h+1 のノードの子は seg が i を含まないので Pi に属さない)、主張が示された。■
以降は、この一意性のもとで、パス Pi の高さ h のノードを単に「Pi の高さ h のノード」と呼び、Nh(Pi) で表します。
セグメント木の構成
木の構造の上に、値としてモノイドを扱う、一点更新・区間取得ができるセグメント木を構成します。
設定
- M=(M,⋅,e) をモノイドとします。
- 管理するモノイドの配列を a=(a0,…,an−1)∈Mn とします。
- 各ノード τ に、モノイドの元 vτ∈M を保持します。
- 不変条件: 常に
vτ=i∈seg(τ)∏ai
が成り立つようにします。∏ はモノイドの演算 ⋅ に関する積をインデックス順に取ったもので、結合則から括弧の付け方に依存しません。
セグメント木の初期化
すべての i について ai←e とし、木の全ノードで vτ←e と初期化します。単位元の積は単位元なので不変条件を満たします。
set(i, c): 一点更新
配列の単一要素の更新 ai←c に対応する操作 set(i, c) を定義します。
まず、 ai←c を行うためにノード τ の値を更新する操作 set(i, c, τ) を下のように定義します。
set(i, c, τ):
if seg(τ) = [i, i+1):
v[τ] ← c # 葉なので直接書き換え
return
if i ∈ seg(lch(τ)):
set(i, c, lch(τ))
else:
set(i, c, rch(τ))
v[τ] ← v[lch(τ)] · v[rch(τ)] # 子の更新後に再計算
そして、set(i, c) を ルートに対する更新 set(i, c, τ0) として定義します。
query(l, r): 区間 [l, r) の総積
区間に配置されたモノイドの元の積に対応する操作を同様に定義します。
query(l, r, τ):
if seg(τ) ∩ [l, r) = ∅:
return e # 単位元(何も無ければ影響しない)
if seg(τ) ⊆ [l, r):
return v[τ] # 部分木を丸ごと使える
return query(l, r, lch(τ)) · query(l, r, rch(τ))
こちらも、query(l, r) を query(l, r, τ0) として定義します。
成り立って欲しい性質
お待たせしました。天下り的に与えた set, query が期待する性質を持つことを確認しましょう。
補題2 (set操作の不変条件保存)
任意の木のノード τ について、(初期状態を含む)任意の set 呼び出し列の後で
vτ=i∈seg(τ)∏aiが成立する。
証明. 呼び出し列の長さに関する帰納法で示す。初期状態ではすべての ai=e かつすべての vτ=e なので、空でない seg(τ) についても ∏ie=e より成立する。
呼び出し列の前まで不変条件が成立していると仮定し、次の呼び出し set(i,c) の後でも成立することを示す。以下、ノード τ の高さ h(τ) に関する帰納法で、「呼び出し set(i,c,τ0) の後、vτ=∏j∈seg(τ)aj(新しい配列に対して)が成り立つ」ことを、すべてのノード τ について示す。
-
h(τ)=0(τ は葉、seg(τ)=[k,k+1) とする):
- k=i の場合、アルゴリズムは vτ←c と直接書き換える。新しい配列では ai=c なので ∏j∈[i,i+1)aj=c=vτ より成立する。
- k=i の場合、i∈/seg(τ) なのでアルゴリズムはこのノードを訪問せず vτ を書き換えない。また k=i より ak も変化しないので ∏j∈[k,k+1)aj の値も変わらない。呼び出し前に不変条件が成り立っていた(帰納法の仮定)ので、呼び出し後も vτ=∏j∈seg(τ)aj が成立する。
-
h(τ)>0: 子を lch(τ),rch(τ) とする。高さに関する帰納法の仮定より、子については主張がすでに成立している、すなわち
vlch(τ)vrch(τ)=j∈seg(lch(τ))∏aj,=j∈seg(rch(τ))∏aj
が(新しい配列に対して)成り立つ。
- i∈seg(τ) の場合、アルゴリズムはこのノードを訪問し、vτ←vlch(τ)⋅vrch(τ) と計算する。seg(τ)=seg(lch(τ))⊔seg(rch(τ))(インデックス順を保った非交和)なので、モノイドの結合則により
vτ=(j∈seg(lch(τ))∏aj)⋅(j∈seg(rch(τ))∏aj)=j∈seg(τ)∏aj.
成立。
- i∈/seg(τ) の場合、アルゴリズムはこのノードを訪問せず vτ を書き換えない。i∈/seg(τ) なので ai の変化は ∏j∈seg(τ)aj に影響しない。呼び出し前の不変条件(帰納法の仮定)より、呼び出し後も vτ=∏j∈seg(τ)aj が成立する。
以上、高さに関する帰納法によりすべてのノード τ で主張が示され、特に呼び出し列に関する外側の帰納法の帰納段も示された。■
ここで使っているのは結合則(と単位元の存在、空積のため)のみで、可換性は不要です。
補題3 (query操作の正当性)
補題2の不変条件が成り立っている状態で、任意のノード τ について
query(l,r,τ)=i∈seg(τ)∩[l,r)∏ai.証明. ノード τ の高さ h(τ) に関する帰納法で示す。
-
h(τ)=0(τ は葉、seg(τ)=[k,k+1) とする):
- k∈/[l,r) の場合、seg(τ)∩[l,r)=∅ なのでアルゴリズムは e を返す。空積の約束により ∏i∈∅ai=e より成立。
- k∈[l,r) の場合、seg(τ)=[k,k+1)⊆[l,r) なのでアルゴリズムは vτ を返す。補題2より vτ=∏i∈[k,k+1)ai、かつ seg(τ)∩[l,r)=[k,k+1) より成立。
(葉では seg(τ)∩[l,r) は ∅ か seg(τ) 自身のいずれかであり、この2ケースで尽くされる。)
-
h(τ)>0(子を lch(τ),rch(τ) とする): 高さに関する帰納法の仮定より、子については主張がすでに成立している、すなわち
query(l,r,lch(τ))query(l,r,rch(τ))=i∈seg(lch(τ))∩[l,r)∏ai,=i∈seg(rch(τ))∩[l,r)∏ai
が成り立つ。
- seg(τ)∩[l,r)=∅ の場合、アルゴリズムは e を返す。空積の約束により ∏i∈∅ai=e より成立。
- seg(τ)⊆[l,r) の場合、アルゴリズムは vτ を返す。補題2より vτ=∏i∈seg(τ)ai、かつ seg(τ)⊆[l,r) より seg(τ)∩[l,r)=seg(τ) より成立。
- それ以外(seg(τ)∩[l,r) が seg(τ) の空でない真部分集合)の場合、子を再帰的に呼び出し query(l,r,lch(τ))⋅query(l,r,rch(τ)) を返す。
seg(τ)∩[l,r)=(seg(lch(τ))∩[l,r))⊔(seg(rch(τ))∩[l,r))
(順序を保った非交和)なので、帰納法の仮定とモノイドの結合則により
==query(l,r,lch(τ))⋅query(l,r,rch(τ))(i∈seg(lch(τ))∩[l,r)∏ai)⋅(i∈seg(rch(τ))∩[l,r)∏ai)i∈seg(τ)∩[l,r)∏ai.
より成立。
以上、高さに関する帰納法によりすべてのノード τ で主張が示された。■
系. seg(τ0)=[0,n)⊇[l,r) なので、
query(l,r)=query(l,r,τ0)=i∈[l,r)∏aiとなります。これが求めたかった性質です。
計算量
次に、計算量です。
set, query はいずれも根から高々 d=log2n 段の再帰で計算できます。ただし query については、単純に「各ノードで定数回」という理由だけでは O(logn) は言えません。訪問されるノードの総数自体を、木の幾何を使って各階層ごとに定数個に抑える必要があります。
補題4 (setの計算量)
ノード τ に対する呼び出し set(i, c, τ) の実行にかかるモノイド演算(積 ⋅ の呼び出し)の回数を Tset(τ) とすると、Tset(τ)≤h(τ)。
証明. h(τ) に関する帰納法。
- h(τ)=0(葉): vτ←c の代入のみでモノイド演算は行われないので Tset(τ)=0=h(τ)。
- h(τ)>0: 子 lch(τ),rch(τ) のうちいずれか一方だけが再帰的に呼ばれる。呼ばれた子を τk とすると、
set はまず set(i, c, τ_k) を呼び、その後 vτ←vlch(τ)⋅vrch(τ) で積を1回計算する。呼ばれなかった子は再帰されない。よって
Tset(τ)=Tset(τk)+1≤h(τk)+1=h(τ)
(最後の不等号は帰納法の仮定、最後の等号は h(τk)=h(τ)−1 による)。■
根 τ0 の高さは d=log2n なので、set(i, c) のモノイド演算の回数は O(logn) です。木を辿る際の分岐判定などの付随する処理も各段で定数回なので、全体でも O(logn) となります。
与えられた配列からのセグメント木の構築は set を n 回呼び出すため O(nlogn) になります。
query の計算量の証明をするために、補題を証明します。
定義 (境界ノード)
ノード τ が [l,r) に対する境界ノードであるとは、 seg(τ)∩[l,r)=∅ かつ seg(τ)⊆[l,r) が成り立つことをいう。
[l,r) に対する境界ノード全体を Bd(l,r) で表す。[l,r) に対する高さ h の境界ノード全体を Bd_h(l,r)=Bd(l,r)∩{τ:h(τ)=h} で表す。
補題5(境界ノードの局在)
ノード τ が境界ノードであるとする。このとき l∈seg(τ) または r−1∈seg(τ) が成り立つ。
証明. seg(τ)=[x,y) と書く(x<y)。境界ノードが存在する以上 [l,r)=∅、すなわち l<r である。
seg(τ)∩[l,r)=∅ より max(x,l)<min(y,r)。seg(τ)⊆[l,r) より x<l または y>r の少なくとも一方が成り立つ。
- x<l の場合: このとき max(x,l)=l なので l<min(y,r)≤y、かつ x<l より x≤l。よって l∈[x,y)=seg(τ)。
- y>r の場合: 同様に min(y,r)=r なので max(x,l)<r、かつ x≤max(x,l)<r より x≤r−1。また y>r より y≥r+1、すなわち r−1<y。よって r−1∈[x,y)=seg(τ)。■
系 (境界ノード数の評価)
境界ノード τ は補題5より l∈seg(τ) または r−1∈seg(τ) を満たす、すなわちパス Pl または Pr−1 上のノードに限られる(境界ノードが存在する時点で l<r なので、r−1 は [0,n) 内の有効なインデックスであることに注意)。
よって τ の高さを h とすると、補題1(パスの一意性)よりそのようなノードはそれぞれ Nh(Pl),Nh(Pr−1) しかない。したがって
Bh(l,r)⊆{Nh(Pl), Nh(Pr−1)}であり、特に
∣Bh(l,r)∣≤2が任意の h∈0,…,d について成り立つ。
補題6(queryの計算量)
query(l, r) の実行全体にかかるモノイド演算(積 ⋅ の呼び出し)の回数は、高々 2(d+1)=O(logn) である。
証明. query(l, r, τ) の呼び出し全体で積が計算されるのは境界ノードにおいてであり、境界ノード1つにつき積はちょうど1回計算される。境界ノードの高さは 0 から d までの d+1 通りで、補題5の系より各高さの境界ノード数は高々2個。よって積の計算回数の総和は高々 2(d+1) となる。■
d=log2n なので、query(l, r) にかかるモノイド演算の回数は O(logn) です。境界ノードでない訪問ノード(第1・第2分岐で即座に返るノード)も、境界ノードの子としてしか生成されないため、その総数も O(logn) に抑えられ、条件判定などの付随する処理も含めて全体で O(logn) となります。
おわりに
いかがだったでしょうか。当たり前のように書いている性質をただ証明しただけではありますが、この記事がセグメント木によって目的の計算ができる仕組みを理解するのに役立てば幸いです。
次回は、今回天下り的に与えた構成がなぜ「自然なもの」であるかについて考えてみたいと思います。お楽しみに。
次回:セグメント木の理屈を学ぶ(2) 不変条件を同型として読み直す
参考文献
入門寄り
- “セグメント木を徹底解説!0から遅延評価やモノイドまで”: セグメント木についての日本語の概要です。
- “Segment Tree”: セグメント木の英語の概要です。具体的な利用方法について詳しく書かれています。
セグメント木の可視化
- “Segment Tree のお勉強(1)”, “Segment Tree のお勉強(2)”: 2べきでない場合、モノイドが可換でない場合も含め、セグメント木と遅延セグメント木で演算をしたときの様子が可視化されています。
データ構造と代数的構造の整合性
- “Testing Calibration in Nearly-Linear Time”: この論文では双対セグメント木に対してデータ構造を具体的に構成した後、代数的構造との整合性を証明しています。この記事の議論の構成はこちらの記事から大きく影響を受けています。
クエリと代数構造の対応
- “セグメント木と代数構造の理論”: この文献は「どのようなクエリがセグメント木で処理できるか」に着目し、セグメント木に乗る代数構造について体系的に考察しています。