離散フーリエ変換 (DFT) および数論変換 (NTT) の原理、そしてそれらのプログラミングにおける実装方法について記述します。
各用語の定義の違いを明確にするため、それぞれについての回を分割します。
今回は、離散フーリエ変換 (DFT) において演算を効率化する高速フーリエ変換 (fast Fourier transform, FFT) と、そのプログラミングについて記述します。
前回に引き続き、多項式をベクトルとして考えることにします。
すなわち、
DFT の定義を再確認しておきます。
このとき、
で表される変換
ここでは逆変換である IDFT については考えず、DFT の高速化を考えます。
すなわち、与えられた
まず、
として、各
この式を観察すると、例えば
実際、
とすると、
の形式となります。
また、
前節における考察を一般化します。
とすると、
と表せます。
次に、
と計算できます。
つまり、
上の式を書き換えて、
のようにも表現できます。
また、
時間計算量については、上記の操作を
以上の考察を定理の形でまとめておきます。
(i)
(ii)
とすると、
また、時間計算量は
この漸化式による計算方法を、高速フーリエ変換 (fast Fourier transform, FFT) と呼ぶ。
上記の考察から、数学的帰納法により成り立つ。
このように、サイズの小さい同じ問題に分割して再帰的に解く方法は分割統治法 (divide-and-conquer algorithm) と呼ばれます。例えば、マージソートも同様の手法によります。
FFT の逆変換である逆高速フーリエ変換 (inverse fast Fourier transform, IFFT) についても、同様の考察から次の漸化式が得られます。
(i)
(ii)
とすると、
FFT と異なる点は、
また、次のように DFT の結果を利用して IDFT の結果を求める方法もあります。
これについては FFT である必要はなく、前回までの議論でも成り立ちます。
により成り立つ。
つまり、DFT の結果に対して、
以上で、DFT および IDFT が高速化され、時間計算量
これにより、畳み込み (多項式の積) も
定理 1 および定理 3 で得られた式をそのまま実装します。
定理 1 の主張が再帰で表現されているため、この実装においても再帰関数を使います。
入力の配列の長さが
Convolution 関数については前回の実装とほぼ同じで、
using System;
using System.Numerics;
public static class FFT
{
public static int ToPowerOf2(int n)
{
var p = 1;
while (p < n) p <<= 1;
return p;
}
// 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);
}
// c の長さは 2 の冪とします。
static void TransformRecursive(Complex[] c)
{
var n = c.Length;
if (n == 1) return;
var n2 = n >> 1;
var c0 = new Complex[n2];
var c1 = new Complex[n2];
for (int k = 0; k < n2; ++k)
{
c0[k] = c[2 * k];
c1[k] = c[2 * k + 1];
}
TransformRecursive(c0);
TransformRecursive(c1);
for (int k = 0; k < n2; ++k)
{
var v0 = c0[k];
var v1 = c1[k] * NthRoot(n, k);
c[k] = v0 + v1;
c[k + n2] = v0 - v1;
}
}
// 戻り値の長さは 2 の冪となります。
public static Complex[] Transform(Complex[] c, bool inverse)
{
if (c == null) throw new ArgumentNullException(nameof(c));
var n = ToPowerOf2(c.Length);
var t = new Complex[n];
c.CopyTo(t, 0);
TransformRecursive(t);
if (inverse && n > 1)
{
Array.Reverse(t, 1, n - 1);
for (int k = 0; k < n; ++k) t[k] /= n;
}
return t;
}
// 戻り値の長さは 2 の冪となります。
public static Complex[] Convolution(Complex[] a, Complex[] b)
{
if (a == null) throw new ArgumentNullException(nameof(a));
if (b == null) throw new ArgumentNullException(nameof(b));
var n = ToPowerOf2(a.Length + b.Length - 1);
Array.Resize(ref a, n);
Array.Resize(ref b, n);
var fa = Transform(a, false);
var fb = Transform(b, false);
for (int k = 0; k < n; ++k)
{
fa[k] *= fb[k];
}
return Transform(fa, true);
}
}
逆変換のみに必要な処理 (定理 3) は、Transform 関数の最後に追加しています。
以下のオンラインジャッジで FFT のコードをテストできます。時間計算量
以上で、再帰関数により FFT を実装できました。数式で表現した通りの実装をすればよいため仕組みがわかりやすいという利点があるものの、この実装では各層で配列を作成して値をコピーしているため、空間計算量も
次回はビット反転置換によりこれを改善します。
前回:
(2) 数列の畳み込み
次回:
(4) ビット反転置換