非再帰FFT
FFT
長さの配列は次多項式と対応付けられる。をの乗根として、配列を、すなわちにおけるの評価値を求めたい。の偶数次、奇数次の係数を取ってくることで作られる2つの次多項式
\begin{align*}
f_0 (x) &= \sum_{0 \le k < 2^{n-1}} A_{2k} x^k\\
f_1 (x) &= \sum_{0 \le k < 2^{n-1}} A_{2k+1} x^k
\end{align*}
を用いることで、は
\begin{align*}
f(x) = f_0(x^2) + x f_1(x^2)
\end{align*}
と分けられる。, に対してFFTをすることで得られるにおける,の評価値を用いることで、各は以下の式から計算できる。
\begin{align*}
f(\zeta^i) &=
\begin{cases}
f_0\left((\zeta^2)^i\right) + \zeta^i f_1\left((\zeta^2)^i\right) & (0 \le i < 2^{n - 1})\\
f_0\left((\zeta^2)^{i - 2^{n-1}}\right) - \zeta^{i - 2^{n-1}} f_1\left((\zeta^2)^{i - 2^{n - 1}}\right) & (2^{n - 1} \le i < 2^n)
\end{cases}
\end{align*}
行列
もう少し機械的に扱いたいので、上の操作が何をやっているかを行列の言葉で表してみる。行列を\begin{align*}
F_n=(\zeta^{ij})_{(i,j)}=
\begin{pmatrix}
\zeta^0 & \zeta^0 & \zeta^0 & \cdots & \zeta^0 \\
\zeta^0 & \zeta^1 & \zeta^2 & & \zeta^{2^n-1} \\
\zeta^0 & \zeta^2 & \zeta^4 & & \zeta^{2(2^n - 1)}\\
\vdots & & & \ddots & \vdots \\
\zeta^0 & \zeta^{2^n-1} & \zeta^{2(2^n-1)} & \cdots & \zeta^{(2^n-1)(2^n-1)}
\end{pmatrix}
\end{align*}
と定めてを行ベクトルと見なすと、FFTはをに変換する操作と言うことができる。
上で説明したFFTは
- をインデックスの偶奇で2つに分けてそれぞれFFTを行う
- それらの結果を組み合わせて答えを求める
\begin{align*}
F_3 &=
\begin{pmatrix}
\zeta^{0} & \zeta^{0} & \zeta^{0} & \zeta^{0} & \zeta^{0} & \zeta^{0} & \zeta^{0} & \zeta^{0} \\
\zeta^{0} & \zeta^{1} & \zeta^{2} & \zeta^{3} & \zeta^{4} & \zeta^{5} & \zeta^{6} & \zeta^{7} \\
\zeta^{0} & \zeta^{2} & \zeta^{4} & \zeta^{6} & \zeta^{8} & \zeta^{10} & \zeta^{12} & \zeta^{14} \\
\zeta^{0} & \zeta^{3} & \zeta^{6} & \zeta^{9} & \zeta^{12} & \zeta^{15} & \zeta^{18} & \zeta^{21} \\
\zeta^{0} & \zeta^{4} & \zeta^{8} & \zeta^{12} & \zeta^{16} & \zeta^{20} & \zeta^{24} & \zeta^{28} \\
\zeta^{0} & \zeta^{5} & \zeta^{10} & \zeta^{15} & \zeta^{20} & \zeta^{25} & \zeta^{30} & \zeta^{35} \\
\zeta^{0} & \zeta^{6} & \zeta^{12} & \zeta^{18} & \zeta^{24} & \zeta^{30} & \zeta^{36} & \zeta^{42} \\
\zeta^{0} & \zeta^{7} & \zeta^{14} & \zeta^{21} & \zeta^{28} & \zeta^{35} & \zeta^{42} & \zeta^{49}
\end{pmatrix} \\
&=
\left(
\begin{pmatrix}
(\zeta^2)^{0} & & (\zeta^2)^{0} & & (\zeta^2)^{0} & & (\zeta^2)^{0} & \\
& & & & & & & \\
(\zeta^2)^{0} & & (\zeta^2)^{1} & & (\zeta^2)^{2} & & (\zeta^2)^{3} & \\
& & & & & & & \\
(\zeta^2)^{0} & & (\zeta^2)^{2} & & (\zeta^2)^{4} & & (\zeta^2)^{6} & \\
& & & & & & & \\
(\zeta^2)^{0} & & (\zeta^2)^{3} & & (\zeta^2)^{6} & & (\zeta^2)^{9} & \\
& & & & & & &
\end{pmatrix}
+
\begin{pmatrix}
& & & & & & & \\
& (\zeta^2)^{0} & & (\zeta^2)^{0} & & (\zeta^2)^{0} & & (\zeta^2)^{0} \\
& & & & & & & \\
& (\zeta^2)^{0} & & (\zeta^2)^{1} & & (\zeta^2)^{2} & & (\zeta^2)^{3} \\
& & & & & & & \\
& (\zeta^2)^{0} & & (\zeta^2)^{2} & & (\zeta^2)^{4} & & (\zeta^2)^{6} \\
& & & & & & & \\
& (\zeta^2)^{0} & & (\zeta^2)^{3} & & (\zeta^2)^{6} & & (\zeta^2)^{9}
\end{pmatrix}
\right) \\
& \qquad \begin{pmatrix}
1&&&& 1&&& \\
\zeta^0&&&& -\zeta^0&&& \\
&1&&& &1&& \\
&\zeta^1&&& &-\zeta^1&& \\
&&1&& &&1& \\
&&\zeta^2&& &&-\zeta^2& \\
&&&1& &&&1 \\
&&&\zeta^3& &&&-\zeta^3 \\
\end{pmatrix}
\end{align*}
このまま色々弄っていくのはつらいので、もうちょっと分かりやすくこれらの行列を表してみたい。
変換を扱いやすくする
まず以下のように記法を定めておく。- bit整数を考えるとき左方をLSBとする。例えばを表す bit整数はとなる。
- bit整数のbit を、bit をなどと書くことにする。
- 適宜bitの連結をそのままつなげることで表すことにする。例えば bit整数に対しと書いたらbit が、bit がである bit整数を表すとする。
- のbit reversalをと書くことにする。例えばとなる。
- の乗根をと書くことにする。例えばを表す bit整数を用いての乗根はと書ける。
(操作1)、(操作2)を表す行列を次のように書くことにする。
(操作1)について
配列のサイズはなので各インデックスは bit整数と見なせる。また配列はペアの集合と見なせる。「偶奇で分けてFFTを行う」というのは「bit の値ごとにグループ分けし、各グループ内でbit をインデックスに用いてFFTを行う」ことであることから、その行列をと書くことにする。(操作2)について
このパートはさらに2つの変換に分解できる。- 各 bit整数に対し、とをとに写す。
- インデックスを1個分左巡回シフトする。つまりをペアの集合と見た時、各をに写した集合に変える。
補足
はクロネッカー積を用いてとも書ける。ただしはある行列を用いてと書けるという主張ではなく、インデックスのbit をの箇所が担当しているという気持ちで書いている。変換を変形していく
これらを用いると、はと表せる。`スワップ'みたいなことを繰り返してを前方に持ってくることを考えてみる。はインデックスのbit列を左巡回シフトする変換であることを踏まえると、は以下のように変形できる。
例えばの場合、これを再帰的に用いると次のように変形できる。
ここで、はbit reversalを表す変換になっている。これをと表すことにすると、
と表せる。のような変換がバタフライ演算と呼ばれる。
bit reversalを右に
以上からbit reversal + バタフライ演算を行うことで非再帰FFTが実装できるが、ここでbit reversalを後ろに持っていくことを考えてみる。がインデックスのbit列を逆順にする変換であることを踏まえると、は以下のように変形できる。ただし、はとは逆順にインデックスのbit列を扱うようにしたものとする。さっきは変換のときにを掛けていたが、今度は逆順に扱うようになったのでが掛けられるようになる。例えばは、とをとに写す。
一番最後に置かれたbit reversalは評価値を正しい順番に並び替えているだけなので、その操作を無視しても畳み込み定理は依然成り立つ。したがって畳み込みの実装において無視してしまって良い。ただし原子根を逆元に取り替えるだけでは逆変換とはならなくなる。逆変換をするには素直に巻き戻しをすれば良い。つまり
の表す変換をすれば良い。
またこのとき、変換後にインデックスに来る値はのにおける評価値になる。これを踏まえての意味を考えてみる。 bit整数 に対し、次多項式を
\begin{align*}
f_i(x) = \sum_{j: n - k \text{ bit整数}} A_{ij}x^j
\end{align*}
と定める*1。例としてインデックスにおける値の変遷に注目してみると、と変換されていき、の添字がの肩に移っていく感じになっている。
逆にこのように変換を進めていこうという気持ちになることで、この非再帰FFTを直接導ける。例えばの添字のbit長をからにするステップにおいてとからとを求めるには、今が成り立つので、
\begin{align*}
f_{1}(1^{0.011}) &= f_{10}(1^{0.11}) + 1^{0.011} f_{11}(1^{0.11}) \\
f_{1}(1^{0.111}) &= f_{10}(1^{0.11}) - 1^{0.011} f_{11}(1^{0.11})
\end{align*}
のようにすれば良い。
ACL
ACLのbutterflyもこのような変換を行っている。高速化のために可能であれば2 bit分ずつ処理するようになっているが、ここでは1 bitバージョンに注目する。int p = 1 << (h - len - 1); mint rot = 1; for (int s = 0; s < (1 << len); s++) { int offset = s << (h - len); for (int i = 0; i < p; i++) { auto l = a[i + offset]; auto r = a[i + offset + p] * rot; a[i + offset] = l + r; a[i + offset + p] = l - r; } if (s + 1 != (1 << len)) rot *= info.rate2[bsf(~(unsigned int)(s))]; }
h-lenが現在の添字のbit長、rotがになっている。
例えばのとき、はの順に回る。そのループ内において各4 bit整数に対しとをとに写している。
rot *= info.rate2[bsf(~(unsigned int)(s))];
のパートが複雑に見えるが、これは単にからを計算している。
例えばのとき、からを、つまりからを得るには、に対しての逆元との積を掛けてやればよい。繰り上がりが止まるbitの位置がbsf(~(unsigned int)(s))によってbit 2と分かる。info.rate2[2]にはの逆元との積が前計算されて入っており、それが参照される。
2 bitバージョンについては、1 bitバージョン2回と考えるほかに、とととをとととに写すようなことをしていると考えることもできる。
転置ver.
は対称行列なので転置しても同じ変換を表す。例えばの転置を取ることで
を得る。ここで、はの転置行列とする。転置を取ることは重みをそのままに「入口」と「出口」を交換することと見なせる。はとをとに写す変換なので、これは以下のような回路を表す。
よってこれの転置となる回路は
となり、はとをとに写す変換になっている。
これもbit reversalが最後に来ているので無視できる。こっちの方針で実装すると以下のようになる。
int p = 1 << (len - 1); const mint root_len = info.root[len]; for (int s = 0; s < (1 << (h - len)); s++) { mint rot = 1; int offset = s << len; for (int i = 0; i < p; i++) { auto l = a[i + offset]; auto r = a[i + offset + p]; a[i + offset] = l + r; a[i + offset + p] = (unsigned long long)(mint::mod() + l.val() - r.val()) * rot.val(); rot *= root_len; } }
空間的局所性を高めるために2重ループの内側で下方bitを回すようにしている。そのため二重ループ内でrotを毎回更新している。手元でACLの1 bitバージョンとこれを比較したが、長さのときに前者は、後者はになって、ACLの方が速い。
*1:はbitの連結の意味