前回、遅延セグメント木を扱いましたが、今回は一旦遅延は忘れてセグメント木に戻り、扱うことができる構造をモノイドから一般化することを考えます。(遅延構造まで考えると手に負えなくなりました)
第2回目の続きに近い内容です。
先行研究
セグメント木に圏が載る、という主張は “セグメント木の上に乗る構造はモノイドではなく圏である” などでなされており、“セグメント木, より一般に「区間積クエリを高速計算する方法」の圏によるモデル” ではセグメント木を半開区間から圏への関手として捉えています。“セグメント木に圏が乗るか?” でも同様の定式化がされています。
今回は本質的にはこれらとかなり近い内容になります。
前提
- 前回までの設定と記法を引き継ぎます。
- 圏、関手、自然変換は定義のレベルで使います。圏はすべて small とします。対象の集合を Ob(C)、射の集合を Mor(C), 射 f の始域・終域を dom(f),cod(f) と書きます。
- 合成は図式順に書きます。すなわち f:X→Y, g:Y→Z に対し f⋅g:X→Z が「まず f、次に g」を表します(通常の g∘f のことです)。前回までの積の記法をそのまま使いたいためです。
具体例から考える: 連鎖行列積
先行研究にも出てくる例を考えます。各 i に Hi×Wi 行列 Ai が乗っていて、Wi=Hi+1 が成り立っているとします。このとき区間 [l,r) の区間積
AlAl+1⋯Ar−1∈KHl×Wr−1は意味を持ちます。一方 A0A2 のような積は一般には定義されないので、Ai たちの全体はモノイドになるとは限りませんが、常に隣接するものどうしの積が計算できれば十分なので、この状況はセグメント木で扱えます。
行列の全体は次のような圏 Mat を成します。
- 対象: 正の整数
- 射 i→j : i 行 j 列の実行列
- 合成: 行列の積(図式順に書いているので、A:i→j と B:j→k の合成 A⋅B はそのまま積 AB)
- 恒等射 idi : i 次単位行列
これが、セグメント木をちょいと拡張してモノイド以外のものも扱えることにしておきたい理由の一つですね。
配列の空間に変わるもの
モノイドで考えていた時は、モノイドからなる任意の配列 Mn が管理できましたが、圏の場合は合成可能な射のみを考える必要があります。
圏 C に対して、 n 個の射の合成可能な鎖の集合を
Nn(C)={a=(a0,…,an−1)∈Mor(C)n∣cod(ai)=dom(ai+1)(0≤i<n−1)}と書きます。a∈Nn(C) に対して対象の列 X0,…,Xn が ai:Xi→Xi+1 によって定まります。区間 [l,r),0≤l≤r≤n に対する区間積は
μ[l,r)(a)=al⋅al+1⋯ar−1:Xl→Xrで、空区間 l=r のときは idXl(=idXr) と約束します。半開区間では区間の両端がそのまま始域・終域になるので、閉区間のときのような「r+1」のずれが消え、空区間の場合の約束も不自然さなく書けます。
C がモノイド(対象がひとつの圏)なら、合成可能性の条件は自動的に満たされるので Nn(C)=Mn となり前回の場合と一致します。
区間からの関手
Nn(C) を区間からの関手と同一視していきます。全順序集合 0<1<⋯<n を圏と見たものを [n] と書きます。i≤j のとき唯一の射 i→j が存在する圏です。そうすると、
Nn(C)≅Fun([n],C)が成り立ちます。関手 F:[n]→C を与えることは、対象 F(0),…,F(n) と、隣接する射 F(i→i+1) を与えることと同じです(それ以外の射の像は合成で決まり、恒等射の像は恒等射に決まります)。
この見方は elliptic-shiho 氏 と p進大好きbot 氏 の両方が採用しているものです。
後者は「セグメント木に乗るのは小圏そのものではなく、区間から小圏への関手である」という形で強調しています。「圏が乗る」と言うと、モノイドのときと同じように「圏の射からなる任意の配列が管理できる」と読めてしまうが、文字通りには正しくない、という指摘です。
状態空間と πd
木の側も定義しておきます。今までと同様に、深さ d の完全二分木のノード集合を Td、葉の数を n=2d とし、内部ノード τ の左右の子を lch(τ),rch(τ) と書きます。各ノード τ には半開区間 seg(τ)=[x,y)⊆[0,n) が対応します。根は seg(τ0)=[0,n)、葉は seg(τ)=[i,i+1) です。
モノイドのときの整合性条件は vlch(τ)⋅vrch(τ)=vτ でした。圏では、この等式を書く前に左辺が意味を持つかどうかを問う必要があります。積 vlch(τ)⋅vrch(τ) が定義されるとは cod(vlch(τ))=dom(vrch(τ)) のことなので、条件をふたつ並べて
Segd(C)=v∈Mor(C)Td∣cod(vlch(τ))=dom(vrch(τ)),vlch(τ)⋅vrch(τ)=vτ(∀τ:内部ノード)と書けます。
ついでに両端の対象を取り出す写像も用意しておきます。v∈Segd(C) に対し根の値 vroot は C の射なので
src(v)=dom(vroot),tgt(v)=cod(vroot)と定めます。鎖の側にも同じ名前で
src(a)=dom(a0),tgt(a)=cod(an−1)を定めておきます。a の定める対象の列 X0,…,Xn でいえば X0 と Xn のことです。
実装の見直し
セグメント木では set, query を下のように定義していました。
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(τ)] # 子の更新後に再計算
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 でモノイドの単位元 e を返していましたが、圏では各対象に対して恒等射があることしか保証されていないので、モノイドのように掛けたときに計算結果を変えない単位元をとりあえず返すようなことができません。
- 任意の射の合成に対して、元々の射を保つ 吸収元 を導入して、 e の代わりに返す: とりあえずこのやり方でセグメント木として動かすことはできます。実装としては、こちらのようになっていることが多そうです。吸収元と書きましたが、射の合成の条件を厳密に考えると怪しいですね。
- 恒等射を返すように、
query 自体を下のように書き換える:
query(l, r):
if l = r:
return id_{X(l)} # ここだけ特別扱い
return query(l, r, τ0)
query(l, r, τ):
if seg(τ) ∩ [l, r) = ∅:
error # 呼び出しを禁止
if seg(τ) ⊆ [l, r):
return v[τ]
if seg(lch(τ)) ∩ [l, r) = ∅:
return query(l, r, rch(τ))
else if seg(rch(τ)) ∩ [l, r) = ∅:
return query(l, r, lch(τ))
else:
return query(l, r, lch(τ)) · query(l, r, rch(τ))
AC Library の Segtree のシグネチャは
(1) segtree<S, op, e> seg(int n)
(2) segtree<S, op, e> seg(vector<S> v)
- 型
S
- 二項演算
S op(S a, S b)
- 単位元
S e()
でした。この S e() を k=0,...,n に対する恒等射 idXk を返す S id(int k) に置き換えて segtree<S, op, id> にするような考えです。
seg(τ)∩[l,r)=∅ となる呼び出しを禁止していますが、正しい呼び出し列の範囲では error になることはありません。呼び出し側で責任を持って使ってくださいという考えですね。
これ以降は2を前提にして考えることにしましょう。すると、前々回の命題7と同様にして状態空間と木の同型が証明できます……とまでは行きません。モノイドのときは深さ d+1 の木を左右の部分木の直積として貼り合わせていましたが、圏では貼り合わせる境界で積が定義されていることを保証するため、直積をファイバー積に取り替えなければいけません。
命題 16
πd:Segd(C)→Nn(C)(葉への制限)は全単射であり、かつ両端の対象と両立する。すなわちすべての v について src(πd(v))=src(v) と tgt(πd(v))=tgt(v) が成り立つ。さらにその逆写像は πd−1(a)τ=μseg(τ)(a) で与えられる。
証明. d についての帰納法で、全単射性と両立性を同時に示す。
d=0 のときは Seg0(C)=Mor(C)=N1(C) で π0 は恒等写像なので、どちらも自明。
d について成り立つとして d+1 を示す。第2回の補題8(再帰的分解)は、深さ d+1 の木の状態を与えることが、深さ d の部分木の状態の組 (vL,vR) であって根どうしの積が定義されるものを与えることと同じということだったが、これは圏でもそのまま成立する。根どうしの積が定義される条件はは tgt(vL)=src(vR) と書けるので、同型
Segd+1(C)≅Segd(C)×Ob(C)Segd(C)が得られる。(右辺は tgt と src に沿ったファイバー積、つまり tgt(vL)=src(vR) を満たす組 (vL,vR) の全体)。
同じ理由で、鎖の側にも前半と後半に切り、cod(a2d−1)=dom(b0) で貼り合わせる
N2d+1(C)≅N2d(C)×Ob(C)N2d(C)が存在する。
ここで制限なしの直積の間の写像 πd×πd:Segd(C)×Segd(C)→N2d(C)×N2d(C) を考えると、帰納法の仮定よりこれは全単射である。あとはこれが両辺のファイバー積の部分に制限されることを言えばよく、そのためには
tgt(vL)=src(vR)⟺tgt(πd(vL))=src(πd(vR))を確かめれば十分。右辺は、帰納法の仮定の両立性を左右それぞれに使えばそのまま左辺となるため、 πd×πd を制限しても全単射。
以上と補題8の同型を合わせれば πd+1 は全単射。 v↔(vL,vR) のとき vroot=(vL)root⋅(vR)root なので src(v)=src(vL), tgt(v)=tgt(vR) であり、鎖の側でも連結した鎖の両端は前半の始域と後半の終域なので、それぞれに帰納法の仮定を使えば両立性も同時に従います。
逆写像が Segd(C) に属することも前回と同様で、親の区間が子の区間の順序を保った disjoint union であることから、親の積が子の積の合成に等しく、特にその合成が定義されていることも同時に分かります。■
積がファイバー積に変えると、両立性を示すために命題に条件を入れる必要が出てきました。 C がモノイドなら Ob(C) は一点なので、ファイバー積は直積に戻って前回の証明と同じになります。
壊れるところ: 更新の定義域が積でない
前回、配列側の更新は
seti:M×Mn→Mnという全域写像でした。木側の Seti はこれを πd で移送したものとして一意に定まりました。
圏では、a∈Nn(C) の i 番目を c に取り替えた列が再び Nn(C) に属するために、c の型が ai と揃っていることが要ります。ai:Xi→Xi+1 だったら取り替える時も c:Xi→Xi+1 の形で書けないと型が合わないということですね。すなわち
dom(c)=dom(ai),cod(c)=cod(ai)です。逆にこれさえ満たしていれば、i 番目以外の隣接関係はどこも変わらないので、取り替えた列は Nn(C) に属します。つまり seti の定義域は直積ではなく、両端の対象を取る写像
∂:Mor(C)→Ob(C)2,∂i:Nn(C)→Ob(C)2,∂(c)=(dom,c,cod,c),∂i(a)=∂(ai)に沿ったファイバー積
seti:Mor(C)×Ob(C)2Nn(C)⟶Nn(C)が正しい形です。ここでも直積がファイバー積に化けていて、 C がモノイドなら Ob(C)2 は一点なので条件は空になり、M×Mn に戻ります。
木側も同じです。πd は葉への制限そのものなので、葉 i の値は ai そのものであり、∂i は木の側でもそのままの意味を持ちます。よって
Seti=πd−1∘seti∘(id×πd):Mor(C)×Ob(C)2Segd(C)⟶Segd(C)と定めれば、前回と同様、πd を通じた同一視のもとで配列側の更新と木側の更新は同じ操作になります。命題8 の全単射を両側の(同じ条件で切り出した)部分集合に制限しているだけですね。
ここで壊れるのが「任意の値に更新できる」という部分で、モノイドのときは好き勝手な値に 1 点更新できましたが、圏では型の合う射にしか取り替えられません。列の途中の対象 Xi を別のものに変えたければ ai−1 と ai を同時に更新するしかなく2点以上更新が必要になりますね。連鎖行列積でいえば、行列の中身は自由に差し替えられますが、行列のサイズを変えるには隣も一緒に差し替える必要があります。
一方、μ[l,r) は Nn(C) 上で全域なので、区間積はそのまま持ち上がります。ただし空区間(l=r)だけは注意が必要で、 idXl を l ごとに識別して返す必要があるので、 query を書き換えていたわけです。
値の置き換えは関手
第2回では、モノイドの準同型 f:M→N に対して成分ごとの適用 Segd(f) が定まり、これが操作と可換することを見ました。「Z 上で計算してから mod m を取っても、最初から Z/m 上で計算しても同じ」でしたね。
圏でこれに対応するものを考えましょう。
命題17
写像 Φ:Mor(C)→Mor(D) が次を満たすとする。
- 成分ごとの適用が Seg1(C)→Seg1(D) を定める。すなわち v∈Seg1(C) ならば Φ(v)∈Seg1(D)。
- 各対象 X について Φ(idX) は D の恒等射である。
このとき、Φ0(X):=dom(Φ(idX)) と定めれば (Φ0,Φ) は関手 C→D である。逆に、関手はすべての d について Segd(C)→Segd(D) を誘導し、区間積と可換する。
証明. 深さ 1 の木、すなわち根とふたつの葉だけからなる木を 3 通りに使う。以下 f:X→Y, g:Y→Z とする。条件 2 より Φ(idX) は恒等射なので、その始域を Φ0(X) と書けば Φ(idX)=idΦ0(X) である。
葉に (idX,f) を置くと、根は idX⋅f=f であり、この状態は Seg1(C) に属する。条件 1 より像 (idΦ0(X),Φ(f))、根 Φ(f) は Seg1(D) に属する。特に idΦ0(X)⋅Φ(f) が定義されるので、domΦ(f)=Φ0(X) を得る。葉に (f,idY) を置けば同様に codΦ(f)=Φ0(Y) である。
葉に (f,g) を置くと根は f⋅g であり、条件 1 より Φ(f⋅g)=Φ(f)⋅Φ(g) を得る。
以上より Φ は始域・終域を Φ0 を通じて保ち、合成と恒等射を保つので関手である。逆は明らかで、関手が合成可能性と合成を保つことから整合性条件が保たれ、区間積との可換性も空区間の場合を恒等射の保存で処理すれば従う。■
対象の写像 Φ0 を与えなくても、深さ 1 のセグメント木が壊れないことを要求するだけで関手としての性質が導出されるのが面白いですね。
前回、モノイドの場合に同じ問いを立てれば、答えは「積を保ち単位元を保つ写像」、すなわち準同型でした。圏でも構造は同じで、条件 1 が合成可能性と合成の保存に、条件 2 が単位元の保存に対応します。
条件 2 は落とせない
条件 2 を落とすと、Φ(idX)=Φ(idX⋅idX)=Φ(idX)⋅Φ(idX) より Φ(idX) は冪等射であることしか言えず、冪等射は一般に恒等射ではありません。
反例: M=N=2×2 実行列(積についてのモノイド)とし、冪等行列 P=I をひとつ取って Φ(A)=P(定数写像)とします。すると Φ(AB)=P=PP=Φ(A)Φ(B) なので積は保たれますが、Φ(I)=P=I です。このとき空区間のクエリは、木側では Φ を通した後に I を返し、配列側では Φ(I)=P を返すので別になりますね。
関手でない「積を保つ写像」たち
Mat 上には、一見「積を保つ」ように見えて条件 1 を満たさない写像がいくらでもあります。
- 転置 A↦AT は (AB)T=BTAT なので、積の順序を逆にします。成分ごとに転置すると親が子の積の逆順になり、Segd(Mat) から出てしまいます。これは Matop への関手であって、木の左右を反転させる操作と組み合わせない限り使えません。前々回から一貫して可換性を仮定してこなかったので、この区別は消えません。
- 行列式 は正方行列でしか乗法的でなく、そもそも Mor(Mat) 全体では定義されていません。「積を保つ」という言明が型の上で意味を持たない例です。
- サイズを揃える(ゼロ埋めして N×N にする)写像は全域で定義されますが、合成可能性を壊します。1×2 と 2×3 の積は、揃えてからの積とは別物です。
これに対し mod m を取る写像は関手です。対象(サイズ)を動かさず、成分ごとに Z→Z/m を適用するだけで、合成可能性も合成も恒等射も保ちます。前回の「値をどこかへ写してから計算してよいのは、その写像が準同型のときだけ」は、圏では「関手のときだけ」に置き換わります。
吸収元を足してモノイドに戻す
実装の立場からは、合成できない組が来たら「未定義」を表す値を返すことにして、全部モノイドとして扱ってしまうのが手軽です。
Mor(C) をただの集合と見て、元 ⊥ と形式的な単位元 e を追加し(恒等射 idX たちは全体の単位元にはならないので、単位元は別途足す必要があります)、合成できない組の積を ⊥ と定めれば、⊥ を吸収元とするモノイドが得られます。p進大好きbot 氏の記事 では Maybe モナドの考え方として紹介されており、tatyam 氏の指摘が出典として挙げられています。
これは正しく動き、前回までの枠組みがそのまま使えますが、得られたモノイドを M⊥ とすると、Segd(M⊥)≅M⊥n であって、右辺には型の合わない配列も入っています。本当に扱いたい Nn(C) はその部分集合にすぎず、意味のある答えが返るのもそこに限られます。要するに、型検査を実行時に遅延させただけです。さきほどの空区間の話も関係していて、 M⊥ 上のクエリが空区間に対して返すのは e であって、idXl ではないです。
p進大好きbot 氏はこの点について、構造を取り替えておいて「圏が乗る」と言うのは、単位元を足しておいて「半群が乗る」と言うのと同じで不正確だ、と述べています。この連載でやってきたことからしても、私も同じ感想で、 M⊥ を載せたセグメント木の状態空間は M⊥n であって Nn(C) ではないという立場です。
まとめ
配列を Nn(C)(区間からの関手)に取り替え、直積が出てくるところをファイバー積に取り替えて、モノイドでやったことと同様のことを関手で考えました。状態空間と木の同型も、更新の移送も、値の置き換えの特徴づけも、形は変わっていません。
変わったのは配列の空間そのものです。モノイドのときは Mn という何の条件もない集合が相手で、だからこそ「モノイドが載る」と言えました。圏では Mor(C)n との全単射にはならず、合成可能性という追加の制約が要ります。その意味で「セグメント木に圏が載る」は言い過ぎだ、というのが私の結論で、これは先行研究の指摘と近いですね。
作用(遅延伝播)の話も同じ枠組みで書けるはずですが、それは気力があればということで、今回はこの辺で筆を置きます。