ググれば大量の記事が出てくるが、知見の整理ということで自分でも書いてみる。
ゼータ・メビウス変換とは
「高速」ゼータ・メビウス変換とは名前の通りゼータ・メビウス変換を高速化したものなので、まずはそちらを理解する必要がある。
定義: ゼータ変換、メビウス変換
を有限集合、を加法が定義された集合1とする。
このとき写像から、以下を満たす写像を求めることをゼータ変換という2。
なお、この式中のをにしたものも同様にゼータ変換と呼ぶことにする3。
そしてこの逆変換、すなわちから以下を満たすを求めることをメビウス変換という4。
この式中のをにすると、上で包含関係の向きを逆にした場合のゼータ変換の逆変換になる。
この変換がゼータ変換の逆変換であることは全く自明ではないが、この記事ではその証明は省略する5。
以降、集合としてのみを考え、関数を長さの配列として表現する。これと対応して、以降をのように表現する。
ゼータ変換の高速化
ここからゼータ変換、特に実装しやすいの方を高速に行うことを考える。メビウス変換も符号を少し変えるだけでほぼ同様に実現できる。
計算量の評価は、加算が行われる回数 (Complexity)によって行う。
愚直解
まず愚直に求めようとすると、以下のようなコードになるだろう。
for (int s = 0; s < (1 << n); ++s) { for (int t = 0; t < (1 << n); ++t) { if ((s & t) == t) g[s] += f[t]; } }
とはの部分集合全体を回り、、すなわちのときだけにを加算する。定義通りである。
の部分集合は計個あることから、このコードの計算量はとなる。
部分集合の列挙
しかしこのコードではの探索に無駄が多すぎる。できることならではなくの部分集合全体だけを回したいが、実はこれは以下のコードで実現できる。
for (int s = 0; s < (1 << n); ++s) { for (int t = s; t >= 0; t = ((t - 1) & s)) { g[s] += f[t]; } }
この手法はビット演算 (bit 演算) の使い方を総特集! 〜 マスクビットから bit DP まで 〜に載っている。
の部分集合は個あり、のサイズの部分集合は個あることから、このコードの計算量はとなる6。
高速ゼータ変換
さらに高速化して、でゼータ変換を行うのが本記事の主題である高速ゼータ変換と呼ばれる手法である。
方針は「下位要素から順に拡張していく」というもの。 まずを「はの部分集合、それより上はと一致しているもの全部の総和」と定義する。言葉では分かりにくいので例を挙げると、
といった具合である。定義より、、。
これをからまで順に更新していく。このとき、か否かで場合分けをする。具体例で考えてみると、
のように、の場合はも足す必要がある。
これを実装に落とし込むと以下の通り。
for (int s = 0; s < (1 << n); ++s) { dp[s][0] = f[s]; } for (int k = 1; k <= n; ++k) { for (int s = 0; s < (1 << n); ++s) { dp[s][k] = dp[s][k - 1]; if ((s >> (k - 1)) & 1) { dp[s][k] += dp[s ^ (1 << (k - 1))][k - 1]; } } } for (int s = 0; s < (1 << n); ++s) { g[s] = dp[s][n]; }
集合及びDPの添字は1-indexedなのに対してbit演算は0-indexedなので、シフト数を1減らしてやる必要がある。
配列の使い回し
さらに更新について考えてみると、DPテーブルは1次元のものを使い回せることに気づく。すなわち、以下のような実装ができる。
for (int s = 0; s < (1 << n); ++s) { dp[s] = f[s]; } for (int k = 1; k <= n; ++k) { // この時点でdp[s]は上の実装のdp[s][k-1]と一致 for (int s = 0; s < (1 << n); ++s) { if ((s >> (k - 1)) & 1) { dp[s] += dp[s ^ (1 << (k - 1))]; } } } for (int s = 0; s < (1 << n); ++s) { g[s] = dp[s]; }
ここで「はより先に更新されているが大丈夫なのか」となるが、なので、この週ではそもそもは更新されていない。よって問題ない。
さらにkを1ずらしたりDP配列として直にgを使ったりして整理することで、他の記事でもよく見られるような実装になる。
for (int s = 0; s < (1 << n); ++s) { g[s] = f[s]; } for (int k = 0; k < n; ++k) { for (int s = 0; s < (1 << n); ++s) { if ((s >> k) & 1) { g[s] += g[s ^ (1 << k)]; } } }
長い道のりだったが、ようやく高速ゼータ変換のコードに辿り着いた。
このコードの計算量は、各について回加算更新が行われるため、となる7。加算がなら、くらいなら安定して間に合うだろう()。
高速ゼータ・メビウス変換の実装例まとめ
最後にまとめとして、各種変換の実装例を載せておく。 なお配列はコード外で宣言済みとする。
(注※) verifyはしていないので、コピペは自己責任で。実装ミスがあれば報告していただけると助かります。
高速ゼータ変換 上位集合版
for (int s = 0; s < (1 << n); ++s) { g[s] = f[s]; } for (int k = 0; k < n; ++k) { for (int s = 0; s < (1 << n); ++s) { if (!((s >> k) & 1)) { g[s] += g[s ^ (1 << k)]; } } }
先程とは逆に、のときにを加える。この遷移はが大きい方から小さい方に加えられるので、更新順序的に問題ない。
ゼータ変換 下位集合版
for (int s = 0; s < (1 << n); ++s) { g[s] = f[s]; } for (int k = 0; k < n; ++k) { for (int s = 0; s < (1 << n); ++s) { if ((s >> k) & 1) { g[s] += g[s ^ (1 << k)]; } } }
先程長々と解説した通り。
メビウス変換 上位集合版
for (int s = 0; s < (1 << n); ++s) { f[s] = g[s]; } for (int k = 0; k < n; ++k) { for (int s = 0; s < (1 << n); ++s) { if (!((s >> k) & 1)) { f[s] -= f[s ^ (1 << k)]; } } }
非自明ではあるが、実はゼータ変換の更新の符号を反転させるだけでいい。 が抜けるとの各項についてが1減るので符号が反転する、みたいなお気持ち。
メビウス変換 下位集合版
for (int s = 0; s < (1 << n); ++s) { f[s] = g[s]; } for (int k = 0; k < n; ++k) { for (int s = 0; s < (1 << n); ++s) { if ((s >> k) & 1) { f[s] -= f[s ^ (1 << k)]; } } }
先と同じく符号を反転させるだけでいい。
-
別な表現として、を満たすような写像をゼータ変換、(存在すれば)その逆写像をメビウス変換と呼ぶこともできる。が、却って分かりにくくなりそうなので割愛。↩
-
この辺は記事によってはメビウス変換になっていたりして曖昧な部分だが、ここでは高速ゼータ変換/高速メビウス変換に従うことにする。↩
-
指数時間アルゴリズム入門の53ページに式変形が載っている。とが逆になっているので注意。↩
-
2つ目の等号は、二項定理でを展開すると成り立つことが分かる。↩