離散フーリエ変換 (DFT) および数論変換 (NTT) の原理、そしてそれらのプログラミングにおける実装方法について記述します。
各用語の定義の違いを明確にするため、それぞれについての回を分割します。
今回は、離散フーリエ変換 (discrete Fourier transform, DFT) の基本的な性質と、プログラミングにおける実装方法について記述します。畳み込みのような応用や、計算の高速化には触れず、まずは原理の記述に焦点を当てます。
この連載では虚数単位を$i$で表し、インデックス (添字) を$k, j$などで表します。
また、$e^{i \theta} = \cos \theta + i \sin \theta$を $\exp i\theta$ で表すことがあります。
(離散でない) フーリエ変換 (Fourier transform, FT) というものがあり、その一部として次の結果があります。
関数$f:[0, 1) \to \mathbf{C}$が次のような三角多項式の形式で表せるとする。
$$ ~~ f(x) = \sum_{k=-n}^{n} c_k \, \exp (2\pi i \, kx) ~~ (c_k \in \mathbf{C}) $$
このとき、
$$ ~~ \widehat{f}(k) = \int_0^1 f(x) \exp (-2\pi i \, kx) \, dx $$
とすると、係数$c_k$は
$$ ~~ c_k = \widehat{f}(k) $$
により定まる。
ここで、$f:[0, 1) \to \mathbf{C}$は半開区間$[0, 1)$から複素数全体への写像を表しています。
フーリエ変換では、このように積分を利用します。フーリエ和をさらにフーリエ級数に発展させると、それが収束すれば周期関数を三角関数の和 (正弦波の重ね合わせ) で表すことが可能になります。
それに対して、積分の代わりに和で扱うものが離散フーリエ変換と言えます。
離散フーリエ変換の定義および定理を記述します。
本稿では、三角多項式を用いる代わりに多項式を用いる定義を扱います。
$n$を正の整数とする。
$n-1$次多項式$f:\mathbf{C} \to \mathbf{C}$は、次の形式で表せる写像とする。
$$ ~~ f(x) = \sum_{0 \leq k < n} c_k \, x^k ~~ (c_k \in \mathbf{C}) $$
すなわち、$n-1$次多項式は$n$個の係数$c_k$のみによって特徴づけられる。
また、必要であれば、$k \geq n$における係数を$c_k=0$とする。
この連載では$c_{n-1} \neq 0$という制約を設けないため、$c_{n-1} = 0$である多項式も便宜上$n-1$次と見なすことができます。文献 [1] では「次数上界が$n$の多項式 (polynomial of degree-bound $n$)」と表現されています。
$n$を正の整数とする。
複素数体$\mathbf{C}$における$1$の原始$n$乗根を、$\omega_n = e^{\frac{2 \pi i}{n}} = \exp \dfrac{2 \pi i}{n}$で表す。
$n-1$次多項式$f:\mathbf{C} \to \mathbf{C}$に対し、$n-1$次多項式$\widehat{f}:\mathbf{C} \to \mathbf{C}$を次のように定義する。
$$ ~~ \widehat{f}(x) := \sum_{0 \leq k < n} f(\omega_n^{~k}) \, x^k $$
$f$に$\widehat{f}$を対応させる写像を、離散フーリエ変換 (discrete Fourier transform, DFT) と呼び、$\mathcal{F}_n$で表す。
DFT が$n$に依存していることを表現するため、記号$\mathcal{F}_n$を導入します。
ここでは、$n$は$2$の冪である必要はありません。
後述する高速フーリエ変換 (FFT) では、$n$は$2$の冪とし、その性質を利用することになります。
文献によっては、
$$ ~~ \widehat{f}(t) := \sum_{0 \leq k < n} f(k) \exp \left( -i \dfrac{2\pi k}{n} t \right) = \sum_{0 \leq k < n} f(k) \, \omega_n^{-kt} $$
のように三角多項式として定義されていることもありますが、この場合は$x = \omega_n^{-t}$とすれば本質的には同じものです。
次の定理の証明の核となる補題を用意します。
$n$を正の整数とする。
$1$の原始$n$乗根$\omega_n \in \mathbf{C}$について、次が成り立つ。
$$
~~ \sum_{0 \leq k < n} \omega_n^{~jk} =
\left\{
\begin{array}{l}
n ~~ (j = 0) \\
0 ~~ (0 < j < n)
\end{array}
\right.
$$
左辺を$S$とすると、$\displaystyle S = \sum_{0 \leq k < n} (\omega_n^{~j})^k$
(i) $j = 0$のとき、$\omega_n^{~j} = 1$だから、
$$ ~~ S = \sum_{0 \leq k < n} 1 = n $$
(ii) $0 < j < n$のとき、$\omega_n^{~j} \neq 1$だから、
$$ ~~ S = \frac{1 - (\omega_n^{~j})^n}{1 - \omega_n^{~j}} = \frac{1 - (\omega_n^{~n})^j}{1 - \omega_n^{~j}} = 0 $$
$n$を正の整数とする。
$n-1$次多項式$f:\mathbf{C} \to \mathbf{C}$に対し、
$$ ~~ f(x) = \frac{1}{n} \sum_{0 \leq k < n} \widehat{f}(\omega_n^{-k}) \, x^k $$
が成り立つ。
$\widehat{f}$に$f$を対応させる写像を、逆離散フーリエ変換 (inverse discrete Fourier transform, IDFT) と呼ぶ。
これは$\mathcal{F}_n$の逆元であるため、$\mathcal{F}_n^{-1}$で表す。
$$ ~~ f(x) = \sum_{0 \leq j < n} c_j \, x^j ~~ (c_j \in \mathbf{C}) $$
とすると、$0 \leq m < n$に対して、
$$ ~~ \widehat{f}(\omega_n^{-m}) = \sum_{0 \leq k < n} \left( \sum_{0 \leq j < n} c_j \, \omega_n^{~kj} \right) \omega_n^{-mk} = \sum_{0 \leq j < n} c_j \left( \sum_{0 \leq k < n} \omega_n^{~k(j-m)} \right) $$
補題より、
$$ ~~ \widehat{f}(\omega_n^{-m}) = n c_m $$
この結果を、冒頭のフーリエ変換の説明のように言い換えてみましょう。
$n$を正の整数とする。
$n-1$次多項式$f:\mathbf{C} \to \mathbf{C}$は、次の形式であるとする。
$$ ~~ f(x) = \sum_{0 \leq k < n} c_k \, x^k ~~ (c_k \in \mathbf{C}) $$
$n$個の係数$c_k$は未知だが、$n$個の値$f(\omega_n^{~k})$は既知であるとする。
このとき、
$$ ~~ \widehat{f}(x) := \sum_{0 \leq k < n} f(\omega_n^{~k}) \, x^k $$
とすると、係数$c_k$は
$$ ~~ c_k = \frac{1}{n} \widehat{f}(\omega_n^{-k}) $$
により定まる。
$n$個の標本点における値をもとに$n$個の係数$c_k$を決定しているということは、つまり$n$元連立$1$次方程式を解いているのです。ここで、標本点として$1$の$n$乗根の集合 $\{ \omega_n^{~k} \}$ を採用すると、$c_k$が美しい形で求められるということです。
$f$の実際の次数が$n-1$より小さい場合でも、範囲外の係数を$0$として$n-1$次多項式と見なすことによりこれまでの議論が成り立ちます。逆に、$f$の次数が$n-1$より大きい場合はこの連立方程式を解けません。
もう一つの要点は、順変換と逆変換に次のような対称性が現れることです。
なお、$\widehat{f}$の定義で$\dfrac{1}{\sqrt{n}}$倍しておくと、逆変換も$\dfrac{1}{\sqrt{n}}$倍となって統一できるのですが、離散フーリエ変換の分野では、逆変換を$\dfrac{1}{n}$倍とする定義が多いようです (おそらく実用上の都合)。
今後、第$k$成分が$v_k$であるベクトルを$(v_k)$で表すことにします。
多項式の定義において、「$n-1$次多項式は$n$個の係数$c_k$のみによって特徴づけられる」と書きました。
したがって、$n-1$次多項式$f:\mathbf{C} \to \mathbf{C}$を、$n$次元ベクトル $(c_k) \in \mathbf{C}^{~n}$ と同一視できます。
これは、線形代数学でよくある考え方です。
ここでは、前節で見た定義や定理をベクトルで表現してみましょう。
$n$を正の整数とする。
$n-1$次多項式$f \in \mathbf{C}^{~n}$に対し、$n-1$次多項式$\widehat{f} \in \mathbf{C}^{~n}$を
$$ ~~ \widehat{f} := (f(\omega_n^{~k})) $$
で定義する。
写像$\mathcal{F}_n: \mathbf{C}^{~n} \ni f \mapsto \widehat{f} \in \mathbf{C}^{~n}$を、離散フーリエ変換 (DFT) と呼ぶ。
$n$を正の整数とする。
$n-1$次多項式$f \in \mathbf{C}^{~n}$に対し、
$$ ~~ f = \left( \frac{1}{n} \widehat{f}(\omega_n^{-k}) \right) $$
が成り立つ。
写像$\mathcal{F}_n^{-1}: \mathbf{C}^{~n} \ni \widehat{f} \mapsto f \in \mathbf{C}^{~n}$を、逆離散フーリエ変換 (IDFT) と呼ぶ。
IDFT の入力と出力だけに注目すると、$\widehat{f}$は任意の入力として考えてよい (記号を変えてよい) ため、まとめると次のようになります。
多項式$f$に対して、DFT により「標本点における$f$の値の集合」に変換し、類似の変換である IDFT により$f$を復元できることが明確になりました。
多項式をベクトル (あるいは数列) として考えることは必須ではありませんが、表記や入出力の観点でだいぶ見通しがよくなります。
プログラミングで実用されるのは後述する高速フーリエ変換 (FFT) ですが、原理の理解のために、まずは高速化せずにナイーブな実装を書くのもよいでしょう。ナイーブな実装は、FFT のテストを保証するためにも使うことができます。
さてここでは、前節で整理した DFT および IDFT を実装していきます。
ともに$\mathbf{C}^{~n}$から$\mathbf{C}^{~n}$への写像であり、$n-1$次多項式の係数$(c_k) \in \mathbf{C}^{~n}$を入力として受け取ります。
計算内容が非常に似ているため、実装する関数を共通化します。
C# では次のように実装できます。$n$次元ベクトルは、長さ$n$の配列で表せばよいです。
using System;
using System.Numerics;
public static class DFT
{
// k 番目の 1 の n 乗根 (n-th root)
static Complex NthRoot(int n, int k)
{
var t = 2 * Math.PI * k / n;
return Complex.FromPolarCoordinates(1, t);
}
// f(ω_n^k) の値
static Complex f(int n, Complex[] c, int k)
{
Complex r = 0;
for (int j = 0; j < c.Length; ++j)
r += c[j] * NthRoot(n, k * j);
return r;
}
// f の係数が整数のとき、f^ の係数も整数になるとは限りません。
public static Complex[] Transform(Complex[] c, bool inverse)
{
if (c == null) throw new ArgumentNullException(nameof(c));
var n = c.Length;
var r = new Complex[n];
for (int k = 0; k < n; ++k)
r[k] = inverse ? f(n, c, -k) / n : f(n, c, k);
return r;
}
}
時間計算量は$O(n^2)$です。FFT ではこれを$O(n \log n)$にすることができます。
ではこの関数を呼び出してみましょう。
using System;
using System.Numerics;
static class Program
{
static void Main()
{
var c1 = new Complex[] { 3, 4, 5, 6, 123 };
// DFT
var t = DFT.Transform(c1, false);
// IDFT
var c2 = DFT.Transform(t, true);
// c1 ≒ c2 (誤差あり)
}
}
c1
は、多項式$3 + 4x + 5x^2 + 6x^3 + 123x^4$を表しています。DFT, IDFT の順に変換すると、出力c2
が入力c1
とほぼ同じ値になることを確認できます。IDFT, DFT の順に実行しても同様です。
ただし、浮動小数点数を利用しているため、誤差が生じることがあります。計算結果が整数とわかっている場合は丸めればよいです。
$f$の各成分が整数だとしても、$\widehat{f}$の各成分は整数になるとは限りません ($\widehat{f}$を丸めないように)。
次回: (2) 数列の畳み込み