こんにちは、20B の @tatyam です。この記事は、traP夏のブログリレー 33日目の記事です。
高速フーリエ変換 といえば、競技プログラミングで数列の畳み込みなどに使われるテクニックですね。
今日は高速フーリエ変換と高速フーリエ変換を利用した畳み込みを習得していきましょう。
ポエム
一般的なフーリエ変換の 定義 / 説明 と異なる部分があるかもしれませんが、競プロであればこれで十分だと思います。
AtCoder では AC Library さえ使えれば FFT は理解しなくて良いことになっていますね。
まあでも、中身を理解するといろいろと応用ができるようになるので、理解する価値はかなり高いでしょう。
離散フーリエ変換 (DFT)
離散フーリエ変換 とは、高速フーリエ変換の元になっているものです。離散フーリエ変換の特殊な場合を高速化したものが高速フーリエ変換というわけですね。
DFT ↓ 高速化 FFT
まずは離散フーリエ変換から理解しましょう。
定義
2 π と書くのはちょっと長いので、τ = 2 π とします。
ζ N を 1 の原始 N 乗根のうちの 1 つ、ζ N = e i τ / N = cos ( N τ ) + i sin ( N τ ) とします。
例えば、ζ 2 = − 1 , ζ 3 = 2 3 − i , ζ 4 = i , ζ 2 N 2 = ζ N , ζ N N = 1 です。
複素平面上の単位円を N 等分する点
数列 A = ( A 0 , A 1 , … , A N − 1 ) から N − 1 次多項式 f A ( x ) = A 0 + A 1 x + ⋯ + A N − 1 x N − 1 を作ります。
畳み込みと多項式の積は対応しているので、この 2 つは同一視しがちです。
そして、A = ( A 0 , A 1 , … , A N − 1 ) を B = ( f A ( ζ N 0 ) , f A ( ζ N 1 ) , … , f A ( ζ N N − 1 ) ) に変換する操作を、離散フーリエ変換と言います。
B 0 B 1 B 2 = f A ( ζ N 0 ) = ζ N 0 A 0 + ζ N 0 A 1 + ζ N 0 A 2 + ⋯ + ζ N 0 A N − 1 = f A ( ζ N 1 ) = ζ N 0 A 0 + ζ N 1 A 1 + ζ N 2 A 2 + ⋯ + ζ N N − 1 A N − 1 = f A ( ζ N 2 ) = ζ N 0 A 0 + ζ N 2 A 1 + ζ N 4 A 2 + ⋯ + ζ N 2 ( N − 1 ) A N − 1 ⋮
⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ B 0 B 1 B 2 ⋮ B N − 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ζ N 0 ζ N 0 ζ N 0 ⋮ ζ N 0 ζ N 0 ζ N 1 ζ N 2 ⋮ ζ N N − 1 ζ N 0 ζ N 2 ζ N 4 ⋮ ζ N 2 ( N − 1 ) ⋯ ⋯ ⋯ ⋱ ⋯ ζ N 0 ζ N N − 1 ζ N 2 ( N − 1 ) ⋮ ζ N ( N − 1 ) 2 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ A 0 A 1 A 2 ⋮ A N − 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤
離散フーリエ変換の行列表現
vector <complex <double >> DFT(vector <complex <double >> A){
const int N = A.size();
vector <complex <double >> B(N);
for (int i = 0 ; i < N; i++) for (int j = 0 ; j < N; j++){
B[i] += A[j] * polar(1 , 2 * M_PI * i * j / N);
}
return B;
}
C++ で離散フーリエ変換
逆離散フーリエ変換
逆に、B = ( f A ( ζ N 0 ) , f A ( ζ N 1 ) , … , f A ( ζ N N − 1 ) ) から A = ( A 0 , A 1 , … , A N − 1 ) を復元する操作を逆離散フーリエ変換と言います。
逆離散フーリエ変換はどのような形になっているでしょうか?
ここで、
B 0 + B 1 + ⋯ + B N − 1 = = ( ζ N 0 + ζ N 0 + ζ N 0 + ⋯ + ζ N 0 ) A 0 + ( ζ N 0 + ζ N 1 + ζ N 2 + ⋯ + ζ N N − 1 ) A 1 + ( ζ N 0 + ζ N 2 + ζ N 4 + ⋯ + ζ N 2 ( N − 1 ) ) A 2 + ⋯ + ( ζ N 0 + ζ N N − 1 + ζ N 2 ( N − 1 ) + ⋯ + ζ N ( N − 1 ) 2 ) A N − 1 N A 0
という式が成り立っています。( A 0 以外の係数は複素平面上の単位円をグルグル回っているので、全部足すと 0 )
よって、A 0 = N 1 ( B 0 + B 1 + ⋯ + B N − 1 ) と復元できます。
また、
ζ N 0 B 0 + ζ N − 1 B 1 + ⋯ + ζ N − N + 1 B N − 1 = = ( ζ N 0 + ζ N − 1 + ζ N − 2 + ⋯ + ζ N − N + 1 ) A 0 + ( ζ N 0 + ζ N 0 + ζ N 0 + ⋯ + ζ N 0 ) A 1 + ( ζ N 0 + ζ N 1 + ζ N 2 + ⋯ + ζ N N − 1 ) A 2 + ⋯ + ( ζ N 0 + ζ N N − 2 + ζ N 2 ( N − 2 ) + ⋯ + ζ N ( N − 1 ) ( N − 2 ) ) A N − 1 N A 1
なので、A 1 = N 1 ( ζ N 0 B 0 + ζ N − 1 B 1 + ⋯ + ζ N − N + 1 B N − 1 ) と復元できます。
同様の計算により、逆離散フーリエ変換は、離散フーリエ変換の ζ N を ζ N − 1 に置き換えて、最後に N で割ったものであることが分かります。
⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ A 0 A 1 A 2 ⋮ A N − 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ = N 1 ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ζ N 0 ζ N 0 ζ N 0 ⋮ ζ N 0 ζ N 0 ζ N − 1 ζ N − 2 ⋮ ζ N − N + 1 ζ N 0 ζ N − 2 ζ N − 4 ⋮ ζ N 2 ( − N + 1 ) ⋯ ⋯ ⋯ ⋱ ⋯ ζ N 0 ζ N − N + 1 ζ N 2 ( − N + 1 ) ⋮ ζ N − ( N − 1 ) 2 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ B 0 B 1 B 2 ⋮ B N − 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤
逆離散フーリエ変換の行列表現
vector <complex <double >> IDFT(vector <complex <double >> B){
const int N = B.size();
vector <complex <double >> A(N);
for (int i = 0 ; i < N; i++) for (int j = 0 ; j < N; j++){
A[i] += B[j] * polar(1 , -2 * M_PI * i * j / N);
}
for (int i = 0 ; i < N; i++) A[i] /= N;
return A;
}
C++ で逆離散フーリエ変換
離散フーリエ変換を利用した畳み込み
数列 A = ( A 0 , A 1 , … , A ∣ A ∣ − 1 ) , B = ( B 0 , B 1 , … , B ∣ B ∣ − 1 ) から、
C k = i + j = k ∑ A i B j
で定義される数列 C = ( C 0 , C 1 , … , C ∣ A ∣ + ∣ B ∣ − 2 ) を作ることを畳み込みと言います。例えば、A = ( 3 , 1 , 4 ) と B = ( 1 , 5 , 9 ) を畳み込むと、C = ( 3 , 1 5 + 1 , 2 7 + 5 + 4 , 9 + 2 0 , 3 6 ) = ( 3 , 1 6 , 3 6 , 2 9 , 3 6 ) となります。
畳み込みは定義式をそのまま計算することで Θ ( ∣ A ∣ ∣ B ∣ ) で計算することができますが、離散フーリエ変換を経由しても計算することができます。
畳み込みと多項式の積
畳み込みの操作は、多項式の積と対応しています。
列 X に対して f X ( x ) を ∣ X ∣ − 1 次多項式 f X ( x ) = X 0 + X 1 x + ⋯ + X ∣ X ∣ − 1 x ∣ X ∣ − 1 とします。
A と B を畳み込んで C ができるとき、f A ( x ) × f B ( x ) = f C ( x ) が成り立ちます。
×
1 x 0
5 x 1
9 x 2
3 x 0
3 x 0
1 5 x 1
2 7 x 2
1 x 1
1 x 1
5 x 2
9 x 3
4 x 2
4 x 2
2 0 x 3
3 6 x 4
i + j = k の条件を x の指数にすることで、x i と x j を掛けたときに x i + j と足し算ができて、同類項が足されて畳み込みになるというわけですね。
多点評価と多項式補間
グラフ上で、1 点を通る 0 次多項式、2 点を通る 1 次多項式、3 点を通る 2 次多項式、… は一意に定まりますね。
f C ( x ) は ∣ A ∣ + ∣ B ∣ − 2 次多項式なので、∣ A ∣ + ∣ B ∣ − 1 個の点でのf C ( x ) の値が分かれば f C ( x ) が分かるということになります。
N = ∣ A ∣ + ∣ B ∣ − 1 と置きます。
N 個の点 ζ N 0 , ζ N 1 , … , ζ N N − 1 を選び、f C ( ζ N 0 ) , f C ( ζ N 1 ) , … , f C ( ζ N N − 1 ) を求めて C を復元することを考えます。これが畳み込みを高速にする秘訣です。
f A ( x ) × f B ( x ) = f C ( x ) であることを思い出すと、f A ( ζ N 0 ) , f A ( ζ N 1 ) , … , f A ( ζ N N − 1 ) と f B ( ζ N 0 ) , f B ( ζ N 1 ) , … , f B ( ζ N N − 1 ) が分かれば、f C ( ζ N 0 ) , f C ( ζ N 1 ) , … , f C ( ζ N N − 1 ) は求まります。
f A ( ζ N 0 ) , f A ( ζ N 1 ) , … , f A ( ζ N N − 1 ) を求めるには、離散フーリエ変換です。長さ N になるまで A の末尾に 0 を追加してから離散フーリエ変換をすれば f A ( ζ N 0 ) , f A ( ζ N 1 ) , … , f A ( ζ N N − 1 ) が出てきます。
同様にして f B ( ζ N 0 ) , f B ( ζ N 1 ) , … , f B ( ζ N N − 1 ) を求めて、掛け算をして f C ( ζ N 0 ) , f C ( ζ N 1 ) , … , f C ( ζ N N − 1 ) を求めます。
最後に、f C ( ζ N 0 ) , f C ( ζ N 1 ) , … , f C ( ζ N N − 1 ) を逆離散フーリエ変換して、C が求まりました。
計算量は、離散フーリエ変換が支配的なので、ここが高速になれば高速な畳み込みが実現できそうです。
高速フーリエ変換 (FFT)
N が 2 冪なら高速になるらしいですよ…
実験してみよう
試しに N = 4 のときで考えてみましょう。
B 0 B 1 B 2 B 3 = A 0 + A 1 + A 2 + A 3 = A 0 + i A 1 − A 2 − i A 3 = A 0 − A 1 + A 2 − A 3 = A 0 − i A 1 − A 2 + i A 3
どこかまとめて計算できる場所はあるでしょうか?
B 0 と B 2 、B 1 と B 3 が似ていますね…
B 0 B 2 B 1 B 3 = A 0 + A 1 + A 2 + A 3 = A 0 − A 1 + A 2 − A 3 = A 0 + i A 1 − A 2 − i A 3 = A 0 − i A 1 − A 2 + i A 3
B 0 B 2 B 1 B 3 = ( A 0 + A 2 ) + ( A 1 + A 3 ) = ( A 0 + A 2 ) − ( A 1 + A 3 ) = ( A 0 − A 2 ) + i ( A 1 − A 3 ) = ( A 0 − A 2 ) − i ( A 1 − A 3 )
A 0 + A 2 , A 0 − A 2 , A 1 + A 3 , A 1 − A 3 の 4 つにまとまりました。
N = 8
N = 8 でもやってみましょう。表記が面倒なので行列を使います。
⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ B 0 B 4 B 1 B 5 B 2 B 6 B 3 B 7 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 4 ζ 8 1 ζ 8 5 ζ 8 2 ζ 8 6 ζ 8 3 ζ 8 7 ζ 8 0 ζ 8 0 ζ 8 2 ζ 8 2 ζ 8 4 ζ 8 4 ζ 8 6 ζ 8 6 ζ 8 0 ζ 8 4 ζ 8 3 ζ 8 7 ζ 8 6 ζ 8 2 ζ 8 1 ζ 8 5 ζ 8 0 ζ 8 0 ζ 8 4 ζ 8 4 ζ 8 0 ζ 8 0 ζ 8 4 ζ 8 4 ζ 8 0 ζ 8 4 ζ 8 5 ζ 8 1 ζ 8 2 ζ 8 6 ζ 8 7 ζ 8 3 ζ 8 0 ζ 8 0 ζ 8 6 ζ 8 6 ζ 8 4 ζ 8 4 ζ 8 2 ζ 8 2 ζ 8 0 ζ 8 4 ζ 8 7 ζ 8 3 ζ 8 6 ζ 8 2 ζ 8 5 ζ 8 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ A 0 A 1 A 2 A 3 A 4 A 5 A 6 A 7 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤
似ている部分を揃えて…
⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ B 0 B 4 B 1 B 5 B 2 B 6 B 3 B 7 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 − ζ 8 0 ζ 8 1 − ζ 8 1 ζ 8 2 − ζ 8 2 ζ 8 3 − ζ 8 3 ζ 8 0 ζ 8 0 ζ 8 2 ζ 8 2 ζ 8 4 ζ 8 4 ζ 8 6 ζ 8 6 ζ 8 0 − ζ 8 0 ζ 8 3 − ζ 8 3 ζ 8 6 − ζ 8 6 ζ 8 1 − ζ 8 1 ζ 8 0 ζ 8 0 ζ 8 4 ζ 8 4 ζ 8 0 ζ 8 0 ζ 8 4 ζ 8 4 ζ 8 0 − ζ 8 0 ζ 8 5 − ζ 8 5 ζ 8 2 − ζ 8 2 ζ 8 7 − ζ 8 7 ζ 8 0 ζ 8 0 ζ 8 6 ζ 8 6 ζ 8 4 ζ 8 4 ζ 8 2 ζ 8 2 ζ 8 0 − ζ 8 0 ζ 8 7 − ζ 8 7 ζ 8 6 − ζ 8 6 ζ 8 5 − ζ 8 5 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ A 0 A 1 A 2 A 3 A 4 A 5 A 6 A 7 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤
マイナスを取り出して…
⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ B 0 B 4 B 1 B 5 B 2 B 6 B 3 B 7 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 2 ζ 8 2 ζ 8 4 ζ 8 4 ζ 8 6 ζ 8 6 ζ 8 0 ζ 8 0 ζ 8 4 ζ 8 4 ζ 8 0 ζ 8 0 ζ 8 4 ζ 8 4 ζ 8 0 ζ 8 0 ζ 8 6 ζ 8 6 ζ 8 4 ζ 8 4 ζ 8 2 ζ 8 2 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎡ A 0 A 2 A 4 A 6 ⎦ ⎥ ⎥ ⎥ ⎤ + ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ζ 8 0 − ζ 8 0 ζ 8 1 − ζ 8 1 ζ 8 2 − ζ 8 2 ζ 8 3 − ζ 8 3 ζ 8 0 − ζ 8 0 ζ 8 3 − ζ 8 3 ζ 8 6 − ζ 8 6 ζ 8 1 − ζ 8 1 ζ 8 0 − ζ 8 0 ζ 8 5 − ζ 8 5 ζ 8 2 − ζ 8 2 ζ 8 7 − ζ 8 7 ζ 8 0 − ζ 8 0 ζ 8 7 − ζ 8 7 ζ 8 6 − ζ 8 6 ζ 8 5 − ζ 8 5 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎡ A 1 A 3 A 5 A 7 ⎦ ⎥ ⎥ ⎥ ⎤
同じ部分が出来ています!
⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ B 0 B 4 B 1 B 5 B 2 B 6 B 3 B 7 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎡ ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 2 ζ 8 4 ζ 8 6 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 6 ζ 8 4 ζ 8 2 ⎦ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎡ A 0 A 2 A 4 A 6 ⎦ ⎥ ⎥ ⎤ + ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ ζ 8 0 − ζ 8 0 0 0 0 0 0 0 0 0 ζ 8 1 − ζ 8 1 0 0 0 0 0 0 0 0 ζ 8 2 − ζ 8 2 0 0 0 0 0 0 0 0 ζ 8 3 − ζ 8 3 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎡ ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 2 ζ 8 4 ζ 8 6 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 6 ζ 8 4 ζ 8 2 ⎦ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎡ A 1 A 3 A 5 A 7 ⎦ ⎥ ⎥ ⎤
同じ部分をまとめた結果、
⎣ ⎢ ⎢ ⎢ ⎡ ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 2 ζ 8 4 ζ 8 6 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 6 ζ 8 4 ζ 8 2 ⎦ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎡ A 0 A 2 A 4 A 6 ⎦ ⎥ ⎥ ⎥ ⎤ と ⎣ ⎢ ⎢ ⎢ ⎡ ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 0 ζ 8 2 ζ 8 4 ζ 8 6 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 4 ζ 8 0 ζ 8 6 ζ 8 4 ζ 8 2 ⎦ ⎥ ⎥ ⎥ ⎤ ⎣ ⎢ ⎢ ⎢ ⎡ A 1 A 3 A 5 A 7 ⎦ ⎥ ⎥ ⎥ ⎤
を計算すれば良いということが分かりました。
これはなんと、離散フーリエ変換の形をしています。(すごい)
長さが半分になっていくので、N が 2 冪であれば、これを繰り返し適用できて、高速になりそうです。
一般化
N を 2 冪とします。A = ( A 0 , A 1 , … , A N − 1 ) を高速フーリエ変換して B = ( B 0 , B 1 , … , B N − 1 ) を作ります。
A を奇数番目と偶数番目に分けて、A e = ( A 0 , A 2 , … , A N − 2 ) , A o = ( A 1 , A 3 , … , A N − 1 ) を作ります。
A e , A o をそれぞれ高速フーリエ変換して、B e , B o とします。 (長さ 1 の場合そのまま)
( B o ) i (0-indexed) を ζ N i 倍します。
B i = ( B e ) i + ( B o ) i , B N / 2 + i = ( B e ) i − ( B o ) i の式で B を作ります。
長さ N の処理を 1 回、長さ N / 2 の処理を 2 回、… 、長さ 1 の処理を N 回行うので、計算量は O ( N log N ) です。
vector <complex <double >> FFT(vector <complex <double >> A){
const int N = A.size();
if (N == 1 ) return A;
vector <complex <double >> even(N / 2 ), odd(N / 2 );
for (int i = 0 ; i < N / 2 ; i++){
even[i] = A[i * 2 ];
odd[i] = A[i * 2 + 1 ];
}
even = FFT(even);
odd = FFT(odd);
for (int i = 0 ; i < N / 2 ; i++){
odd[i] *= polar(1.0 , 2 * M_PI * i / N);
A[i] = even[i] + odd[i];
A[N / 2 + i] = even[i] - odd[i];
}
return A;
}
C++ で高速フーリエ変換
FFT には定数倍高速化の技術がたくさんあり、ここから何倍か速くできるのですが、それは他の記事等に任せることにします。
逆高速フーリエ変換
高速フーリエ変換の ζ N を ζ N − 1 に置き換えて、最後に N で割ったものが逆高速フーリエ変換です。
高速フーリエ変換を利用した畳み込み
基本的には離散フーリエ変換を利用した畳み込みと同じで、N が 2 冪になるように、N = ∣ A ∣ + ∣ B ∣ − 1 とするのではなく、∣ A ∣ + ∣ B ∣ − 1 を 2 冪に切り上げたものを N とすれば良いです。
おわり
🎉 これであなたも FFT の理解者! 🎉
おまけ
巡回畳み込み
離散フーリエ変換で畳み込みをするとき、∣ A ∣ + ∣ B ∣ − 1 の長さを確保しますが、もしこれが足りなかったら、すなわち N < ∣ A ∣ + ∣ B ∣ − 1 のときどうなるでしょうか?
f A ( ζ N 0 ) , f A ( ζ N 1 ) , … , f A ( ζ N N − 1 ) と f B ( ζ N 0 ) , f B ( ζ N 1 ) , … , f B ( ζ N N − 1 ) には特に影響はないので、f C ( ζ N 0 ) , f C ( ζ N 1 ) , … , f C ( ζ N N − 1 ) までは正しく計算できますね。しかし、f C は N − 1 次多項式として扱われ、長さ N の C が逆離散フーリエ変換によって出てきます。このときの C はどのようなものになっているでしょうか?
例えば、f A ( x ) × f B ( x ) = x N のとき、f C ( ζ N 0 ) = f C ( ζ N 1 ) = ⋯ = f C ( ζ N N − 1 ) = 1 で、これは f A ( x ) × f B ( x ) = 1 の場合と区別ができません。そのため、C = ( 1 , 0 , … , 0 ) となります。
また、f A ( x ) × f B ( x ) = x N + 1 のとき、f C ( ζ N 0 ) = ζ N 0 , f C ( ζ N 1 ) = ζ N 1 , … , f C ( ζ N N − 1 ) = ζ N N − 1 で、これは f A ( x ) × f B ( x ) = x の場合と区別ができません。そのため、C = ( 0 , 1 , 0 , … , 0 ) となります。
このように、N 次以上の項は、x N = 1 のルールにより x 0 の方から再び現れます。
このような場合の畳み込み
C k = i + j = k ( m o d N ) ∑ A i B j
を巡回畳み込みと言います。離散フーリエ変換は畳み込みをするための変換というより、巡回畳み込みをするための変換(十分な長さを確保することで通常の畳み込みになる)なんですね。
明日の担当は @liquid1224 さんです!楽しみ〜