1
大学数学基礎解説
文献あり

離散フーリエ変換と数論変換 (3) 高速フーリエ変換 (FFT)

864
0

この連載について

離散フーリエ変換 (DFT) および数論変換 (NTT) の原理、そしてそれらのプログラミングにおける実装方法について記述します。
各用語の定義の違いを明確にするため、それぞれについての回を分割します。

  1. 離散フーリエ変換 (DFT)
  2. 数列の畳み込み
  3. 高速フーリエ変換 (FFT) (本稿)
  4. ビット反転置換
  5. 数論変換 (NTT)
  6. NTT の高速化
  7. 中国剰余定理による値の復元

本文

今回は、離散フーリエ変換 (DFT) において演算を効率化する高速フーリエ変換 (fast Fourier transform, FFT) と、そのプログラミングについて記述します。

前回に引き続き、多項式をベクトルとして考えることにします。
すなわち、n1次多項式はC nに属します。

DFT の定義を再確認しておきます。

離散フーリエ変換 再確認

nを正の整数とする。
1の原始n乗根を ωn=exp2πin で表す。

n1次多項式f=(ck)C nは、次の形式で表せる写像とする。
  f(x)=0k<nckxk
このとき、
  Fn(f)=(f(ωn k))C n
で表される変換Fnを離散フーリエ変換 (DFT) と呼ぶ。

実験

ここでは逆変換である IDFT については考えず、DFT の高速化を考えます。
すなわち、与えられた(ck)に対して(f(ωn k))を効率的に求める方法を考察していきます。
まず、n=4として実験してみましょう。

  f(x)=c0+c1x+c2x2+c3x3
として、各f(ω4 k)を変形してみると、
  F4(f)=(f(1)f(i)f(1)f(i))=((c0+c2)+1(c1+c3)(c0c2)+i(c1c3)(c0+c2)1(c1+c3)(c0c2)i(c1c3))
この式を観察すると、例えばc0+c2i(c1c3)など共通する部分を再利用できそうです。またこの部分が、長さが半分の DFT であるF2の形となっていることから、効果的な短縮ができそうだと予想できます。

実際、
  g0(x):=c0+c2x  g1(x):=c1+c3x
とすると、
  f(x)=c0+c2x2+x(c1+c3x2)=g0(x2)+xg1(x2)
の形式となります。
また、(ω4 k)2=ω2 kであることから、F2(g0)およびF2(g1)の各成分が現れます。

高速フーリエ変換

前節における考察を一般化します。
n2の冪とします。

n>1のとき、
  g0(x):=c0+c2x+c4x2+=0k<n/2c2kxk  g1(x):=c1+c3x+c5x2+=0k<n/2c2k+1xk
とすると、g0,g1C n/2であり、
  f(x)=g0(x2)+xg1(x2)
と表せます。

次に、Fn(f)=(f(ωn k))のそれぞれの成分を求めていくと、0k<n/2に対して、
  f(ωn k)=g0(ωn/2 k)+ωn kg1(ωn/2 k)  f(ωn k+n/2)=f(ωn k)=g0(ωn/2 k)ωn kg1(ωn/2 k)
と計算できます。
つまり、Fn(f)を前後半に分割して考えると、Fn/2(g0)=(g0(ωn/2 k))およびFn/2(g1)=(g1(ωn/2 k))が既知であれば、Fn(f)を時間計算量O(n)で求められます。

上の式を書き換えて、
  Fn(f)k=Fn/2(g0)k+ωn kFn/2(g1)k  Fn(f)k+n/2=Fn/2(g0)kωn kFn/2(g1)k
のようにも表現できます。

また、n=1のときはF1(f)=c0Cとすればよいです。
時間計算量については、上記の操作をn2の冪のときだけ実行するため、O(nlogn)となります。

以上の考察を定理の形でまとめておきます。

高速フーリエ変換

n2の冪とする。
n1次多項式f=(ck)C nに対し、Fn(f)C nを求めるための漸化式は、次で与えられる。

(i) n=1のとき、F1(f)=c0C
(ii) n>1のとき、
  {g0:=(c2k)C n/2g1:=(c2k+1)C n/2
とすると、
  {Fn(f)k=Fn/2(g0)k+ωn kFn/2(g1)kFn(f)k+n/2=Fn/2(g0)kωn kFn/2(g1)k  (0k<n/2)

また、時間計算量はO(nlogn)である。
この漸化式による計算方法を、高速フーリエ変換 (fast Fourier transform, FFT) と呼ぶ。

上記の考察から、数学的帰納法により成り立つ。

このように、サイズの小さい同じ問題に分割して再帰的に解く方法は分割統治法 (divide-and-conquer algorithm) と呼ばれます。例えば、マージソートも同様の手法によります。

FFT の逆変換である逆高速フーリエ変換 (inverse fast Fourier transform, IFFT) についても、同様の考察から次の漸化式が得られます。

逆高速フーリエ変換

n2の冪とする。
n1次多項式f=(ck)C nに対し、Fn1(f)C nを求めるための漸化式は、次で与えられる。

(i) n=1のとき、F11(f)=c0C
(ii) n>1のとき、
  {g0:=(c2k)C n/2g1:=(c2k+1)C n/2
とすると、
  {Fn1(f)k=12(Fn/21(g0)k+ωnkFn/21(g1)k)Fn1(f)k+n/2=12(Fn/21(g0)kωnkFn/21(g1)k)  (0k<n/2)

FFT と異なる点は、ωn kの代わりにωnkを使うことと、各階層ごとに計算結果を2で割ることです。

また、次のように DFT の結果を利用して IDFT の結果を求める方法もあります。
これについては FFT である必要はなく、前回までの議論でも成り立ちます。

逆離散フーリエ変換 別解

nを正の整数とする。
n1次多項式f=(ck)C nに対し、Fn1(f)C nは、次の式で求められる。
  Fn1(f)k={1nFn(f)0   (k=0)1nFn(f)nk   (0<k<n)

  Fn1(f)=(1nf(ωnk))=(1nf(ωn nk))
により成り立つ。

つまり、DFT の結果に対して、0<k<nの範囲を逆順に並べ替え、nで割れば IDFT の結果になるということです。

以上で、DFT および IDFT が高速化され、時間計算量O(nlogn)で実行できることがわかりました。
これにより、畳み込み (多項式の積) もO(nlogn)に高速化されます。

プログラミング

定理 1 および定理 3 で得られた式をそのまま実装します。
定理 1 の主張が再帰で表現されているため、この実装においても再帰関数を使います。

入力の配列の長さが2の冪でない場合、それを超える最小の2の冪をnとします。戻り値の配列の長さもそのnとなります。
Convolution 関数については前回の実装とほぼ同じで、n2の冪に変更する部分のみ異なります。

      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 のコードをテストできます。時間計算量O(n2)のナイーブ解では通らず、O(nlogn)の解で通ります。

作成したサンプル コード

以上で、再帰関数により FFT を実装できました。数式で表現した通りの実装をすればよいため仕組みがわかりやすいという利点があるものの、この実装では各層で配列を作成して値をコピーしているため、空間計算量もO(nlogn)となります。
次回はビット反転置換によりこれを改善します。

前回: (2) 数列の畳み込み
次回: (4) ビット反転置換

参考文献

投稿日:202197
OptHub AI Competition

この記事を高評価した人

高評価したユーザはいません

この記事に送られたバッジ

バッジはありません。
バッチを贈って投稿者を応援しよう

バッチを贈ると投稿者に現金やAmazonのギフトカードが還元されます。

投稿者

Researcher and Developer for Algorithms, using C# (.NET). 数検1級金賞 (2002).

コメント

他の人のコメント

コメントはありません。
読み込み中...
読み込み中
  1. この連載について
  2. 本文
  3. 実験
  4. 高速フーリエ変換
  5. プログラミング
  6. 参考文献