myumori diary

Live like a cat

多項式の掛け算をFFTでやるやつを実装

この記事は「みゅーもり Advent Calendar 2018」の4日目分となります:

adventar.org

こんなにふざけたタイトルのAdvent Calendarが25日分すべて埋まるとは正直思っていませんでした. 登録してくださったみなさま, 本当にありがとうございます.

さて今日の内容は表題の通り, FFTを使って多項式同士の積の計算を高速化しようという話です.

専攻によっては学部の講義で扱う有名事実だそうですが, 無知なわたしはつい先日まで知りませんでしたし, これほど単純な問題に対して計算量を改善するテクニックがあるのかと驚いた話でもあったので, まとめておこうと思います.

Jupyter Notebook: http://nbviewer.jupyter.org/github/myuuuuun/blog_contents/blob/master/notebooks/polynomial_multiplication.ipynb

追記

  • (2018/12/5)添字のミスを修正しました. @stmfj さんありがとうございます!

問題設定

 f, g: \mathbb{R} \to \mathbb{R} n-1多項式

\begin{align} f(x) = a_0 + a_1 x + a_2 x^{2} + \ldots + a_{n - 1} x^{n - 1}, \\ g(x) = b_0 + b_1 x + b_2 x^{2} + \ldots + b_{n - 1} x^{n - 1} \end{align}

とする(  a_0, \ldots, a_{n - 1}, b_0, \ldots, b_{n - 1} \in \mathbb{R}*1. このとき, 2つの多項式の積  h = f \cdot g 2n-2多項式 \begin{align} h(x) = c_0 + c_1 x + c_2 x^{2} + \ldots + c_{2n-2} x^{2n-2} \end{align} となる( c_0, \ldots, c_{2n-2} \in \mathbb{R}). この  c_0, c_1, \ldots, c_{2n-2} を求め, 多項式  hを計算したい.

 f(x) = x^{2} + 2x + 3,  g(x) = 3x + 4 のとき,  h = f \cdot gは \begin{align} h(x) = 3x^{3} + 10 x^{2} + 17x + 12 \end{align} となる

素直な解法

単純に考えれば,  h k次の係数 c_kは \begin{align} c_k = \sum_{i=0}^{k} a_i b_{k-i} \end{align} で与えられるので, これを k=0, 1, \ldots, 2n-2について計算をすればよいでしょう. Pythonで実装すると

import numpy as np


def get_poly_product(f_coefs, g_coefs):
    """
    Parameters:
        f_coefs: array_like. fの係数のリスト. [0次の係数, 1次の係数, ..., 最高次の係数]
        g_coefs: array_like. gの係数のリスト. [0次の係数, 1次の係数, ..., 最高次の係数]
    Returns:
        h_coefs: ndarray. h=fgの係数のリスト. [0次の係数, 1次の係数, ..., 最高次の係数]
    """
    f_size, g_size = len(f_coefs), len(g_coefs)
    h_coefs = np.zeros([f_size+g_size-1], dtype=float)
    
    for i in range(f_size):
        for j in range(g_size):
            h_coefs[i+j] += f_coefs[i] * g_coefs[j]
            
    return h_coefs

のようになります. 上の例題を解いてみると

get_poly_product([3, 2, 1], [4, 3])

>> array([12., 17., 10.,  3.])

となり, 手で解いた結果と一致しました.

計算量

コードから明らかなように, この方法の計算量は  O(n^{2}) になります. さて, これよりも少ない計算量で  h の係数を求めることはできるでしょうか?

実は以下で説明するように, FFT高速フーリエ変換)を用いることによって計算量のオーダーを  O(n \log n) に改善することができます.

多項式補間を用いた解法

いま

相異なる k+1個の点  (x_0, y_0), (x_1, y_1), \ldots, (x_k, y_k) を通る k多項式は一意に存在する

という多項式の性質を用い, 次のような手順で hを計算することを考えます:

  • Step 1: 相異なる 2n-1個の点  x_0, x_1, \ldots, x_{2n-2} を任意にとり,  f(x_k), g(x_k) k = 0, 1, \ldots, 2n-2)を計算する
  • Step 2:  h(x_k) = f(x_k) \times g(x_k) k = 0, 1, \ldots, 2n-2)を計算する
  • Step 3:  h(x_0), h(x_1), \ldots, h(x_{2n-2}) を通る一意な 2n-2多項式が求める hなので, 点の座標から  c_0, c_1, \ldots, c_{2n-2} を逆算する

ここでVandermonde行列 \begin{align} V = \left(\begin{array}{ccccc} 1 & x_0 & x_0^{2} & \ldots & x_0^{2n-2} \\ 1 & x_1 & x_1^{2} & \ldots & x_1^{2n-2} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_{2n-2} & x_{2n-2}^{2} & \ldots & x_{2n-2}^{2n-2} \end{array}\right) \end{align} ( 2n-1次正方行列)を導入します. また a_n = a_{n+1} = \ldots = a_{2n - 2} = b_n = b_{n+1} = \ldots = b_{2n - 2} = 0とし, \begin{align} \mathbf{a} = \left(\begin{array}{c} a_0 \\ a_1 \\ \vdots \\ a_{2n-2} \end{array}\right), \ \mathbf{b} = \left(\begin{array}{c} b_0 \\ b_1 \\ \vdots \\ b_{2n-2} \end{array}\right) \end{align} とします.

するとstep 1の計算は \begin{align} \left(\begin{array}{c} f(x_0) \\ f(x_1) \\ \vdots \\ f(x_{2n-2}) \end{array}\right) = V \ \mathbf{a}, \hspace{1em} \left(\begin{array}{c} g(x_0) \\ g(x_1) \\ \vdots \\ g(x_{2n-2}) \end{array}\right) = V \ \mathbf{b} \end{align} のような行列の掛け算に帰着できます.

また (2n - 1) \times (2n - 1) Vandermonde行列の行列式が \begin{align} \det(V) = \prod_{0 \leq s < t\leq 2n - 2} (x_t - x_s) \end{align} であるという有名事実から,  x_kが相異なれば  \det(V) \neq 0 となり,  V逆行列を持ちます. よってstep 3の計算も \begin{align} \left(\begin{array}{c} c_0 \\ c_1 \\ \vdots \\ c_{2n-2} \end{array}\right) = V^{-1} \ \left(\begin{array}{c} h(x_0) \\ h(x_1) \\ \vdots \\ h(x_{2n-2}) \end{array}\right) \end{align} という連立一次方程式の問題に帰着できます.

以上をまとめると, \begin{align} \left(\begin{array}{c} c_0 \\ c_1 \\ \vdots \\ c_{2n-2} \end{array}\right) = V^{-1} \ \left( V \ \mathbf{a} \circ V \ \mathbf{b} \right) \end{align} となります. ただし  \circ はベクトルの要素積です.

計算量

上の解法の計算量を各step毎に考えてみます.


前準備

まずstep 1, step 3の両方で使うVandermonde行列 Vを先に求めておきます. 各 x_kに対し,  x_k^{2}, x_k^{3}, \ldots, x_k^{2n-2}を求めるには 2n-3回の掛け算が必要になるので, 合計の計算回数は \begin{align} (2n - 1) \times (2n - 3) \end{align} 回程度になり, オーダーは O(n^{2})になります

Step 1

 f, gそれぞれに対して行列とベクトルの掛け算を行います.  Vの各行に対してそれぞれ 2n - 1スカラーの掛け算を行うので, 合計の計算回数は \begin{align} 2 \times (2n - 1) \times (2n - 1) \end{align} 回程度になり, オーダーは O(n^{2})になります

Step 2

 k = 0, 1, \ldots, 2n-2 に対して  h(x_k) = f(x_k) \times g(x_k) を求めればよいので, 計算回数は 2n-1回, オーダーは O(n)です

Step 3

 k変数の連立一次方程式をLU分解で解く場合の計算量は O(\frac{2}{3} k^{3})程度なので,  k = 2n-1である今回も計算量のオーダーは O(n^{3})になります


以上をまとめると, 多項式補間を用いた解法の計算量は \begin{align} O(n^{2}) + O(n^{2}) + O(n) + O(n^{3}) = O(n^{3}) \end{align} 程度となり, 素直な解法よりも遅くなってしまいました*2.

しかし x_0, \ldots, x_{2n-2}任意にとってよいという性質を活かすことで, 各stepでの計算量を大幅にへらすことができます.

多項式補間 + DFT(離散フーリエ変換)を使った解法

以降  f, g, h の定義域と値域を \mathbb{C}に拡張して考えます.

前準備の改良

まず標本点として  x_k = (\omega_{2n-1})^{k} k = 0, 1, \ldots, 2n-2)をとってきます. ただし \begin{align} \omega_t = e^{2 \pi i / t} \end{align} です( i虚数単位). このとき \begin{align} \omega_{t}^{k} = \exp \left(\frac{2 \pi k}{t} i \right) = \cos \left(\frac{2 \pi k}{t} \right) + i \sin \left(\frac{2 \pi k}{t} \right) \end{align} となります.

たとえば t=5のケースを複素平面上にプロットすると, 以下のようになります.

f:id:myumori:20181201202545j:plain
 \omega_{5}^{0}, \omega_{5}^{1}, \ldots, \omega_{5}^{4}複素平面上にプロット

いま \omega_tのべき乗は t周期になっている, すなわち \begin{align} \forall \lambda \in \mathbb{Z}, \ \omega_t^{\lambda t + k} = \omega_t^{k} \end{align} が成り立つので, 上記のように定めた  x_0, \ldots, x_{2n-2} に対するVandermonde行列は, 次のように簡単に求めることができます:

 \omega \equiv \omega_{2n-1} とすると, \begin{align} V = \left(\begin{array}{ccccc} 1 & \omega^{0 \times 1} & \omega^{0 \times 2} & \ldots & \omega^{0 \times (2n - 2)} \\ 1 & \omega^{1 \times 1} & \omega^{1 \times 2} & \ldots & \omega^{1 \times (2n - 2)} \\ 1 & \omega^{2 \times 1} & \omega^{2 \times 2} & \ldots & \omega^{2 \times (2n - 2)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{(2n - 2) \times 1} & \omega^{(2n - 2) \times 2} & \ldots & \omega^{(2n - 2) \times (2n - 2)} \end{array}\right) \\ = \left(\begin{array}{ccccc} 1 & \omega^{0} & \omega^{0} & \ldots & \omega^{0} \\ 1 & \omega^{1} & \omega^{2} & \ldots & \omega^{2n - 2} \\ 1 & \omega^{2} & \omega^{4} & \ldots & \omega^{2n - 3} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{2n - 2} & \omega^{2n - 3} & \ldots & \omega^{0} \end{array}\right) \end{align} すなわち  V (j, k)-要素( j, k \in \{ 0, 1, \ldots, 2n-2 \})が \begin{align} V_{jk} = \omega^{k^{\prime}} \hspace{1em} \text{s.t.} \hspace{1em} k^{\prime} & \in \{ 0, 1, \ldots, 2n-2 \} \tag{1} \\ k^{\prime} & \equiv j \times k \text{ (mod $2n-1$)} \end{align} となります.

なおこのVandermonde行列  V をDFT行列といい,  V \mathbf{x} \in \mathbb{C}^{2n-1}に左から掛けることを離散フーリエ変換,  V^{-1} \mathbf{x}に左から掛けることを離散フーリエ逆変換といいます(詳細は後述).

 Vの要素に出てくる値は  \omega^{0}, \omega^{1}, \ldots, \omega^{2n - 2} だけなので, 各 x_kに対して陽にその2乗, 3乗, ...を計算する必要はなくなりました. しかしmodの計算をする必要があるため, 計算量自体は  O(n^{2}) のままです.

Step 3の改良

実は U \equiv \frac{1}{\sqrt{2n - 1}} V はユニタリ行列になっています. すなわち \begin{align} U^{*} U = U U^{*} = I \end{align} が成り立ちます( I単位行列で,  X^{*}は行列 Xの共役転置を表します).


(証明)

まず \omega^{k}複素共役 \bar{\omega^{k}}は, \begin{align} \bar{\omega^{k}} = \omega^{(2n - 1) - k} = \omega^{ - k} \end{align} となる(実数軸に関して線対称な位置への移動).

ここで行列 X j行目を切り出した行ベクトルを X(j, \cdot),  k列目を切り出した列ベクトルを X(\cdot, k)とする( j, k \in \{ 0, 1, \ldots, 2n-2 \}). このとき \begin{align} U^{*}(j, \cdot) \ U(\cdot, k) & = \frac{1}{2n - 1} \left(\begin{array}{ccccc} (\bar{\omega^{j}})^{0} & (\bar{\omega^{j}})^{1} &(\bar{\omega^{j}})^{2} & \ldots & (\bar{\omega^{j}})^{2n - 2} \end{array}\right) \left(\begin{array}{c} \omega^{0 \times k} \\ \omega^{1 \times k} \\ \omega^{2 \times k} \\ \vdots \\ \omega^{(2n - 2) \times k} \end{array}\right) \\ & = \frac{1}{2n - 1} \left(\begin{array}{ccccc} \omega^{- j \times 0} & \omega^{- j \times 1} & \omega^{- j \times 2} & \ldots & \omega^{- j \times (2n - 2)} \end{array}\right) \left(\begin{array}{c} \omega^{0 \times k} \\ \omega^{1 \times k} \\ \omega^{2 \times k} \\ \vdots \\ \omega^{(2n - 2) \times k} \end{array}\right) \\ & = \frac{1}{2n - 1} \sum_{t=0}^{2n - 2} \omega^{t \times (k - j)} \\ & = \begin{cases} \cfrac{1}{2n - 1} \cfrac{1 - (\omega^{k - j})^{2n - 1} }{1 - \omega^{k - j}} & \text{if $j \neq k$}\\ \cfrac{1}{2n - 1} (\sum_{t=0}^{2n - 2} 1) & \text{if $j = k$} \end{cases} \\ & = \begin{cases} 0 & \text{if $j \neq k$}\\ 1 & \text{if $j = k$} \end{cases} \end{align}  j \neq kのケースの最後の変形は,  (k - j) \times (2n - 1) \equiv 0 (mod  2n - 1)より, \begin{align} (\omega^{k - j})^{2n - 1} = \omega^{0} = 1 \end{align} となることを用いた.

 U(j, \cdot) \ U^{*}(\cdot, k) = Iに関しても, 全く同様にして示すことができる.


したがって \begin{align} V^{-1} = \frac{1}{\sqrt{2n - 1}} U^{-1} = \frac{1}{\sqrt{2n - 1}} U^{ * } = \frac{1}{2n - 1} V^{ * } \end{align} となり,  V逆行列を計算する処理は,  Vの共役転置を取る操作に置き換えられます. これを用いるとstep 3の計算量は

  •  V^{*}の計算に O((2n - 1)^{2}) = O(n^{2})
  •  V^{*} とベクトル  \left( h(x_0), \ldots, h(x_{2n - 2}) \right)^{\text{T}} の掛け算に O(n^{2})

かかるので, 合わせて  O(n^{2}) に改善します.

したがって前処理〜step 3までの全体の計算量のオーダーは \begin{align} O(n^{2}) + O(n^{2}) + O(n) + O(n^{2}) = O(n^{2}) \end{align} となります. ただこれでもまだ「単純な方法」と同じオーダーになってしまう(かつこちらの方が複雑な処理をする分時間がかかる)ので, さらに改良して O(n \log n)のオーダーを目指します.

余談: DFTについて

離散フーリエ変換の意味について少し補足をしておきます.  \mathbf{x} \in \mathbb{C}^{k}とし,  k \times kのDFT行列を F_kとします.  \mathbf{x}を離散フーリエ変換した結果が \mathbf{y} \in \mathbb{C}^{k}であるとすると, 上の議論から \begin{align} \mathbf{y} = F_k \mathbf{x} & \Longleftrightarrow F_k^{- 1} \mathbf{y} = I \mathbf{x} \\ & \Longleftrightarrow \cfrac{1}{k} F_k^{*} \mathbf{y} = I \mathbf{x} \\ & \Longleftrightarrow \cfrac{1}{k} \ \left(\begin{array}{ccccc} 1 & 1 & 1 & \ldots & 1 \\ \omega^{- 0} & \omega^{- 1} & \omega^{- 2} & \ldots & \omega^{- (k - 1)} \\ \omega^{- 0} & \omega^{- 2} & \omega^{- 4} & \ldots & \omega^{- (k - 1) \times 2} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ \omega^{- 0} & \omega^{- (k - 1)} & \omega^{- (k - 1) \times 2} & \ldots & \omega^{- (k - 1) \times (k - 1)} \end{array}\right) \mathbf{y} = I \mathbf{x} \\ & \Longleftrightarrow y_0 \phi_0 + \ldots + y_{k - 1} \phi_{k - 1} = x_0 e_0 + \ldots x_{k - 1} e_{k - 1} \end{align} が成り立ちます. ただし \begin{align} \phi_j = \cfrac{1}{k} \ \left(\begin{array}{c} \omega^{- j \times 0} \\ \omega^{- j \times 1} \\ \omega^{- j \times 2} \\ \vdots \\ \omega^{- j \times (k - 1)} \end{array}\right) \end{align} であり,  e_j j番目の要素が1, それ以外が0のベクトルです.

したがって離散フーリエ変換は, 標準基底での座標 \mathbf{x}を,  \{ \phi_0, \phi_1, \ldots, \phi_{k - 1} \}を基底としたときの座標に, 座標変換する操作になります.

なお  \phi_s^{*} \phi_t = \mathbb{1} \{ s = t \} であることから,  \{ \phi_0, \phi_1, \ldots, \phi_{k - 1} \} \mathbb{C}^{k}の正規直交基底になります.

多項式補間 + FFTを使った解法

分割統治法を使って離散フーリエ変換を高速化しようというのが, FFT高速フーリエ変換)の考え方です.

いま  kは2のべき乗 であると仮定し,  \mathbf{x} \in \mathbb{C}^{k}フーリエ変換した結果である  \mathbf{y} = F_k \mathbf{x} を求めることを考えます( kが2のべき乗でない場合は,  kよりも大きい2のべき乗の中で一番小さいものを kと再定義し, それに合わせて \mathbf{x}に0を付け足せばよいです).

まず事実として, 2の倍数であるような tに対しては \begin{align} \omega_t^{2} = \exp \left( 2 \times \frac{2 \pi}{t} i \right) = \exp \left( \frac{2 \pi}{t / 2} i \right) = \omega_{t / 2} \tag{2} \end{align} が成り立ちます.

次に行列 F_kの偶数番号列だけを抜き出した行列を F_k^{\text{even}}, 奇数番号列だけを抜き出した行列を F_k^{\text{odd}} とします. すなわち \begin{align} F_k^{\text{even}} & = \left(\begin{array}{ccccc} 1 & \omega^{0 \times 2} & \omega^{0 \times 4} & \ldots & \omega^{0 \times (k - 2)} \\ 1 & \omega^{1 \times 2} & \omega^{1 \times 4} & \ldots & \omega^{1 \times (k - 2)} \\ 1 & \omega^{2 \times 2} & \omega^{2 \times 4} & \ldots & \omega^{2 \times (k - 2)} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega^{(k - 1) \times 2} & \omega^{(k - 1) \times 4} & \ldots & \omega^{(k - 1) \times (k - 2)} \end{array}\right) \\ F_k^{\text{odd}} & = \left(\begin{array}{cccc} \omega^{0 \times 1} & \omega^{0 \times 3} & \ldots & \omega^{0 \times (k - 1)} \\ \omega^{1 \times 1} & \omega^{1 \times 3} & \ldots & \omega^{1 \times (k - 1)} \\ \omega^{2 \times 1} & \omega^{2 \times 3} & \ldots & \omega^{2 \times (k - 1)} \\ \vdots & \vdots & \ddots & \vdots \\ \omega^{(k - 1) \times 1} & \omega^{(k - 1) \times 3} & \ldots & \omega^{(k - 1) \times (k - 1)} \end{array}\right) \end{align} とし,  F_k^{\prime} = [F_k^{\text{even}}, \ F_k^{\text{odd}}] と定義します.

同様に \mathbf{x}の偶数番号行だけ抜き出したベクトルを \mathbf{x}^{\text{even}}, 奇数番号行だけ抜き出したベクトルをそれぞれ \mathbf{x}^{\text{odd}}とし, \begin{align} \mathbf{x}^{\prime} = \left(\begin{array}{c} \mathbf{x}^{\text{even}} \\ \mathbf{x}^{\text{odd}} \end{array}\right) \end{align} とします.

このとき定義より \begin{align} \mathbf{y} = F_k^{\prime} \mathbf{x}^{\prime} \end{align} が成り立ちます.  \mathbf{x}から \mathbf{x}^{\prime}を計算するコストは O(k)なので,  F_k^{\prime} \mathbf{x}^{\prime} の演算さえ O(k \log k)で実現できれば, 全体の計算量も O(k \log k)にできます.

いま F_k^{\text{even}}は \begin{align} F_k^{\text{even}} = \left(\begin{array}{ccc} 1 & \ldots & \omega_k^{0 \times (k - 2)} \\ \vdots & \ddots & \vdots \\ 1 & \ldots & \omega_k^{(k/2 - 1) \times (k - 2)} \\ \hline 1 & \ldots & \omega_k^{k/2 \times (k - 2)} \\ \vdots & \ddots & \vdots \\ 1 & \ldots & \omega_k^{(k - 1) \times (k - 2)} \end{array}\right) & = \left(\begin{array}{ccc} 1 & \ldots & \omega_{k/2}^{0 \times (k/2 - 1)} \\ \vdots & \ddots & \vdots \\ 1 & \ldots & \omega_{k/2}^{(k/2 - 1) \times (k/2 - 1)} \\ \hline 1 & \ldots & \omega_{k/2}^{k/2 \times (k/2 - 1)} \\ \vdots & \ddots & \vdots \\ 1 & \ldots & \omega_{k/2}^{(k - 1) \times (k/2 - 1)} \end{array}\right) \hspace{0.5em} \text{($\because$ (2))} \\ & = \left(\begin{array}{ccc} 1 & \ldots & \omega_{k/2}^{0 \times (k/2 - 1)} \\ \vdots & \ddots & \vdots \\ 1 & \ldots & \omega_{k/2}^{(k/2 - 1) \times (k/2 - 1)} \\ \hline 1 & \ldots & \omega_{k/2}^{0 \times (k/2 - 1)} \\ \vdots & \ddots & \vdots \\ 1 & \ldots & \omega_{k/2}^{(k/2 - 1) \times (k/2 - 1)} \end{array}\right) \hspace{0.5em} \text{($\because$ (1))} \\ & = \left(\begin{array}{c} F^{\prime}_{k/2} \\ F^{\prime}_{k/2} \end{array}\right) \end{align} と変形できます. 一方 D = \text{diag}(1, \omega_k, \omega_k^{2}, \ldots, \omega_k^{k/2 - 1})とすると,  F_k^{\text{odd}}も \begin{align} F_k^{\text{odd}} & = \left(\begin{array}{ccc} \omega_k^{0 \times 1} & \ldots & \omega_k^{0 \times (k - 1)} \\ \vdots & \ddots & \vdots \\ \omega_k^{(k/2 - 1) \times 1} & \ldots & \omega_k^{(k/2 - 1) \times (k - 1)} \\ \hline \omega_k^{k/2 \times 1} & \ldots & \omega_k^{k/2 \times (k - 1)} \\ \vdots & \ddots & \vdots \\ \omega_k^{(k - 1) \times 1} & \ldots & \omega_k^{(k - 1) \times (k - 1)} \end{array}\right) \\ & = \left(\begin{array}{ccc} \omega_k^{0 \times 1} & \ldots & \omega_k^{0 \times (k - 1)} \\ \vdots & \ddots & \vdots \\ \omega_k^{(k/2 - 1) \times 1} & \ldots & \omega_k^{(k/2 - 1) \times (k - 1)} \\ \hline \omega_k^{(k - k/2) \times 1} & \ldots & \omega_k^{(k - k/2) \times (k - 1)} \\ \vdots & \ddots & \vdots \\ \omega_k^{(k - (k - 1) ) \times 1} & \ldots & \omega_k^{(k - (k - 1) ) \times (k - 1)} \end{array}\right) \\ & = \left(\begin{array}{ccc} \omega_k^{0 \times 1} & \ldots & \omega_k^{0 \times (k - 1)} \\ \vdots & \ddots & \vdots \\ \omega_k^{(k/2 - 1) \times 1} & \ldots & \omega_k^{(k/2 - 1) \times (k - 1)} \\ \hline - \omega_k^{0 \times 1} & \ldots & - \omega_k^{0 \times (k - 1)} \\ \vdots & \ddots & \vdots \\ - \omega_k^{(k/2 - 1) \times 1} & \ldots & - \omega_k^{(k/2 - 1) \times (k - 1)} \end{array}\right) \\ & = \left(\begin{array}{c} D F^{\prime}_{k/2} \\ \hline - D F^{\prime}_{k/2} \end{array}\right) \end{align} と変形できます. 3番目の等式は, 複素単位円上で原点対称な位置への移動を行っていることから来ています.

まとめると \begin{align} F_k = [F_k^{\text{even}}, F_k^{\text{odd}}] = \left(\begin{array}{cc} F_{k/2} & D F_{k/2} \\ F_{k/2} & - D F_{k/2} \end{array}\right) \end{align} となるので, \begin{align} \left(\begin{array}{c} y_{0} \\ \vdots \\ y_{k/2 - 1} \end{array} \right) & = F_{k/2} \mathbf{x}^{\text{even}} + D F_{k/2} \mathbf{x}^{\text{odd}} \\ \left(\begin{array}{c} y_{k/2} \\ \vdots \\ y_{k - 1} \end{array} \right) & = F_{k/2} \mathbf{x}^{\text{even}} - D F_{k/2} \mathbf{x}^{\text{odd}} \end{align} が成り立ちます. よって k次元ベクトルにFFTを行う操作は,

  1.  \mathbf{x}を奇数列と偶数列に分割する操作( O(k)
  2.  k/2次元ベクトルに対しFFTを行う計算2つ(計算量は後述)
  3.  D  F_{k/2} \mathbf{x}^{\text{odd}} の計算( Dが対角行列なので O(k)で可能)
  4. ベクトルの和/差を計算する操作( O(k)

に分割できます

FFTの計算量

 \mathbf{x} k次元の時にFFTを行う計算量を T(k)とします. すると上の分割計算の手順より, 十分大きな kに対して \begin{align} T(k) \leq 2 T(k/2) + c k \end{align} という関係が成り立ちます( c > 0). また1点に対するFFTは元の値そのものなので,  T(1) = O(1)です. この時, \begin{align} T(k) = O(k \log k) \end{align} が成り立ちます(帰納法で証明できます).

高速フーリエ逆変換に関しても同様の分割統治法で計算でき, 計算量のオーダーは O(k \log k)です.

アルゴリズム全体の計算量

長々と説明してきましたが, 結局のところ hを求める手順としては

  • Step 1:  \mathbf{a} および  \mathbf{b}高速フーリエ変換する( O(n \log n)
  • Step 2: 変換したベクトル  V\mathbf{a}, V\mathbf{b} 同士の要素積  V\mathbf{a} \circ V\mathbf{b} を計算する( O(n)
  • Step 3:  V\mathbf{a} \circ V\mathbf{b} を高速フーリエ逆変換する( O(n \log n)

となります. したがってトータルの計算量は O(n \log n)となり, 単純な方法の場合よりも少ない計算量を実現することができました.

実装

これをPythonで実装すると, 以下のようになります.

def get_poly_product_fft(f_coefs, g_coefs):
    """
    Parameters:
        f_coefs: array_like. fの係数のリスト. [0次の係数, 1次の係数, ..., 最高次の係数]
        g_coefs: array_like. gの係数のリスト. [0次の係数, 1次の係数, ..., 最高次の係数]
    Returns:
        h_coefs: ndarray. h=fgの係数のリスト. [0次の係数, 1次の係数, ..., 最高次の係数]
    """
    h_size = len(f_coefs) + len(g_coefs) - 1
    h_fft_coefs = np.fft.fft(f_coefs, n=h_size) * np.fft.fft(g_coefs, n=h_size)
    h_coefs = np.fft.ifft(h_fft_coefs)
    
    return np.real(h_coefs)

例題を解いてみると,

get_poly_product_fft([3, 2, 1], [4, 3])
>> array([12., 17., 10.,  3.])

となり, 手で解いた結果と一致しました.

速度比較: 単純な方法 vs 多項式補間 + FFTを使った解法

私は「多項式補間 + FFT」で hを求める方法を初めて聞いたとき, 「いくら計算量のオーダーが小さくなるといっても, こんなに複雑なことをして計算が速くなるのか?」と疑問に思いました(はじめて聞いた人は普通そう思うでしょう).

というわけで簡単に実験をしてみました *3 *4:

f:id:myumori:20181203211534p:plain
speed comparison

結論: FFTしゅごい.

*1: f, gの次数が違う場合は, 大きい方の次数を n-1とし, 小さい次数の多項式の係数を適当に 0にすれば問題ありません

*2:またVandermonde行列は条件数が大きい行列として有名で, step 3の線形方程式をそのまま解くのは得策ではありません

*3: 本当はFFTも自分で実装するつもりでしたが, 面倒くさくなりました

*4:  2^{12}付近にkinkがあるのはどうしてなんでしょう