フヌリ゚倉換を䜿甚した倚項匏の高速乗算は簡単です

こんばんは

この投皿は高速フヌリ゚倉換に぀いおです。 耇玠数での盎接および逆倉換が考慮されたす。 次のパヌトでは、olympiadプログラミングの問題特に、文字列の「類䌌性」に関する1぀の問題でのアプリケヌションの怜蚎ず、敎数での倉換の実装に぀いお説明する予定です。

FFTは、時間O n⋅log n のnポむントで次数n = 2 kの倚項匏の倀を蚈算するアルゎリズムです「ナむヌブ」メ゜ッドは時間O  n 2 で同じタスクを実行したす。 同時に、逆倉換を実行できたす。 数倀の配列の加算、枛算、乗算は倚項匏よりもはるかに簡単なので特に乗算、FFTは倚項匏ず長い数倀を䜿甚した蚈算を高速化するためによく䜿甚されたす。

定矩ず甚途



たず、倚項匏ずは䜕かを定矩したしょう。

P  x = a 0 + x a 1 + x 2 a 2 + x 3 a 3 + ... + x n - 1 a n - 1

耇玠数



耇玠数に粟通しおいる堎合は、この点をスキップできたす。そうでない堎合は、簡単な定矩を以䞋に瀺したす。

x = a + i b 、ここでi 2 = -1

ここで、aは実郚 Real 郚ず呌ばれ、 bは虚郚 Imaginary 郚です。 ご芧のずおり、これらの数から負のそしお実際に数から根を抜出するこずができたす-これは倚項匏を扱う堎合に非垞に䟿利です-代数の䞻定理から、 

平面䞊の点の圢でそれらを衚珟するこずも非垞に䟿利です。



耇玠数のもう1぀の泚目すべき特性は、 x =cosα+ isinα rで衚珟できるこずです。ここで、αは「数倀」の極角 匕数ず呌ばれたす 、 rはれロからそれたでの距離 モゞュラス です。 そしお、2぀の数字を掛けるずき

a =cosα+ i⋅sinα r a

b =cosβ+ i⋅sinβ r b

a b =cosα+ i⋅sinαcosβ+ i⋅sinβ r a r b

a b =cosα⋅cosβ-sinα⋅sinβ+ i sinα⋅cosβ+cosβ⋅sinα r a r b

a b =cosα+β+ i⋅sinα+β r a r b

モゞュヌルが乗算され、匕数が远加されたす。

1の耇雑な根



次に、 1からのn次の耇雑な根がどのように芋えるかを理解したしょう。 x n = 1ずするず、そのモゞュヌルは明らかに1に等しく、n⋅argx = 2πk、ここでkは敎数です。 これは、 nがそれ自䜓に数を乗算した埌぀たり、 n乗、その匕数が「耇数」2π360床になるこずを意味したす。

匕数ずモゞュヌルがわかっおいる堎合、数倀の匏を思い出しおください。

α= 2π⋅x / n 、ここで0 x

ωi =cosα+ i⋅sinα

぀たり 描画するず、等間隔で円䞊の点のみが埗られたす。



積極的に䜿甚する3぀の点に泚意しおくださいこれらがないず、䜕も機胜したせん。

ωa⋅ωb =ω  a + b mod n

ω0 +ω1 +ω2 + ... +ωn - 1 = 0

ω0 n / 2 =ω2 n / 2 =ω4 n / 2 = ... = 1 偶数nの堎合 

これらの特性のため、これらのポむントで倚項匏の倀を怜蚎したす。 もちろん、結果は必ずしも珟実のものではないため、プログラムは耇玠数を扱う必芁がありたす。

根の合蚈がれロである理由



蚌明は非垞に簡単ですletφ= ω0 +ω1 + .... 䞡偎にω1= 1を掛けたす。 なぜなら ωi⋅ω1 =ωi + 1 、次にφ⋅ω1 =ω1 +ω2 + ... +ωn - 1 + ω0 。 項の再配眮から、合蚈は倉化しないため、それぞれφ=φ⋅ω1、φ⋅ω1-1= 0です。 なぜなら ω1= 1、次にφ= 0 。

仕組み



倚項匏の次数はn = 2 kであるず仮定したす。 そうでない堎合は、先行する係数をれロで最も近い2のべき乗で補いたす。

FFTの基本的な考え方は非垞に簡単です。

させおください

A  x = a 0 + x a 2 + x 2 a 4 + ... + x n / 2-1 a n - 2 偶数の係数P 

B  x = a 1 + x a 3 + x 2 a 5 + ... + x n / 2-1 a n - 1 奇数係数P 

次に、 P  x = A  x 2 + x⋅B x 2 。

ここで、「分割埁服」の原理を適甚したす。n点 ω0 、ω1、 ... でPの倀を蚈算するには、 n / 2点 ω0 、ω2、 ... でAずBの倀を再垰的に蚈算したす。 P ωiの倀は回埩するのが非垞に簡単です

P ωi= A ω2 i +ωi⋅Bω2 i 

次数n / 2の倚項匏の倀を考慮する点をΟi =ω2 iで衚すず、匏は倉換されたす。

P ωi= A Οi+ωi⋅BΟi

iが0からn - 1たでの倀を取り、Οi が 0からn / 2-1たでしか定矩されおいないこずを忘れるこずなく、プログラムに既に駆動できたす。 結論-n / 2を法ずするiを取る必芁がありたす。

動䜜時間は、回垰匏T  n = O  n + 2 T  n / 2 で衚されたす。 これはかなりよく知られた関係であり、 O n⋅log 2 n で明らかになりたす倧たかに蚀うず、再垰の深さはlog 2 nレベルであり、各レベルですべおの呌び出しで合蚈O  n 操䜜が実行されたす。

䜕かを曞く



非効率的な再垰的FFT実装の䟋を次に瀺したす。

遅いfft
#include <vector> #include <complex> using namespace std; typedef complex<double> cd; // STL-  .   double,     sin  cos typedef vector<cd> vcd; vcd fft(const vcd &as) { //       1 int n = as.size(); // -    ? if (n == 1) return vcd(1, as[0]); vcd w(n); //   for (int i = 0; i < n; i++) { double alpha = 2 * M_PI * i / n; w[i] = cd(cos(alpha), sin(alpha)); } //   A  B vcd A(n / 2), B(n / 2); for (int i = 0; i < n / 2; i++) { A[i] = as[i * 2]; B[i] = as[i * 2 + 1]; } vcd Av = fft(A); vcd Bv = fft(B); vcd res(n); for (int i = 0; i < n; i++) res[i] = Av[i % (n / 2)] + w[i] * Bv[i % (n / 2)]; return res; }
      
      





I / Oを远加しお、実装が正しいこずを確認できたす。 倚項匏P  x = 4 + 3 x + 2 x 2 + x 3 + 0⋅x 4 + 0⋅x 5 + 0⋅x 6 + 0⋅x 7の堎合、倀は次のようになりたす。

P  w 0 = 1 0. 0 0 0、0 . 0 0 0 

P  w 1 = 5. 4 1 4、4 . 8 2 8 

P  w 2 = 2. 0 0 0、2 . 0 0 0 

P  w 3 = 2. 5 8 6、0. 8 2 8 

P  w 4 = 2. 0 0 0、0 . 0 0 0 

P  w 5 = 2. 5 8 6 、 -0. 8 2 8 

P  w 6 = 2. 0 0 0 、 -2. 0 0 0 

P  w 7 = 5. 4 1 4 、 -4 . 8 2 8 

その堎合、倧芏暡なテストで再垰的で玠朎な方法を䜿甚できたす。

次数2 12の倚項匏では、この実装は62ミリ秒で動䜜し、玠朎な倚項匏は1800ミリ秒で動䜜したす。 違いは明らかです。

再垰を取り陀く



手順を非再垰的にするには、それに぀いお考える必芁がありたす。 私にずっお最も簡単な方法は、MergeSortマヌゞ゜ヌトずの類䌌性を描き、すべおの再垰呌び出しを瀺す図を描くこずです。



ご芧のずおり、1぀の配列を䜜成し、最初に倀fft a 0 、fft a 4 、fft a 2 、 ...で埋めるこずができたす。 数字a iは、バむナリ衚珟で「拡匵」された数字「 0、1、2、3 、 ... 」であるこずを理解するのは簡単です。 たずえば、 1 1 0 = 0 0 1 2、4 1 0 = 1 0 0 2たたは6 = 1 1 0 2、3 = 0 1 1 2 。 これは次のように理解できたす。再垰の䞋䜍レベルに䞋降するずき、もう1぀の䞋䜍ビットを最埌から決定したす。 「通垞の」番号付けでは、ビットは最初から決定されたす。 したがっお、数倀を「拡匵」する必芁がありたす。 これは、 O  n forelog 2 n の「額」で行うこずも、次のアルゎリズムを䜿甚しおO  n に察しお動的にプログラムするこずもできたす。

  1. 0からn - 1のサむクルを実行したす
  2. 数倀の最䞊䜍ナニットビットの数を栌玍し、動的に再カりントしたす。 珟圚の数が2の环乗である堎合にのみ倉曎されたす。1ず぀増加したす。
  3. 数倀の最䞊䜍ビットがわかっおいる堎合、敎数を反転するこずは難しくありたせん。最䞊䜍ビットXORを「切り捚お」、残り既に蚈算された倀を反転し、「切り捚お」単䜍を远加したす


ここで、「ステップ」からより高いステップを取埗できるアルゎリズムを考え出したす。 前の手順のすべおの倀を1぀の配列に保存したす。 図から明らかなように、最初にk = 1であるkのブロックでデヌタを凊理する必芁があり、その埌、各ステップで2倍になりたす。 長さkの 2぀のブロックを凊理し、出力で長さ2 kの 1぀のブロックを取埗したす。 これがどのように再垰的に行われたかの䟋を芋お、蚘事の最初から匏を思い出しお繰り返しおみたしょう。



2぀のブロックをマヌゞするためのプロシヌゞャの匕数は、2぀のベクトルもちろん、参照、゜ヌス、結果、最初のブロックの開始芁玠の数2番目のブロックの盎埌、ブロックの長さです。 もちろん、むテレヌタでも実行できたす-より倚くのSTL'nostiに぀いおは、簡朔にするためにこの手順をメむンの手順内に転送したす。

ブロックマヌゞ
 void fft_merge(const vcd &src, vcd &dest, int start, int len) { int p1 = start; //     int en1 = start + len; //    int p2 = start + len; //     int en2 = star + len * 2; //    int pdest = start; //      int nlen = len * 2; //    for (int i = 0; i < nlen; i++) { double alpha = 2 * M_PI * i / nlen; cd w = cd(cos(alpha), sin(alpha)); //   dest[pdest] = src[p1] + w * src[p2]; if (++p1 >= en1) p1 = start; if (++p2 >= en2) p2 = start + len; } }
      
      





そしお、䞻な倉換手順

 vcd fft(const vcd &as) { int n = as.size(); int k = 0; //  n   while ((1 << k) < n) k++; vi rev(n); rev[0] = 0; int high1 = -1; for (int i = 1; i < n; i++) { if ((i & (i - 1)) == 0) //    .  i  ,  i-1     . high1++; rev[i] = rev[i ^ (1 << high1)]; //   rev[i] |= (1 << (k - high1 - 1)); //    } vcd cur(n); for (int i = 0; i < n; i++) cur[i] = as[rev[i]]; for (int len = 1; len < n; len <<= 1) { vcd ncur(n); for (int i = 0; i < n; i += len * 2) fft_merge(cur, ncur, i, len); cur.swap(ncur); } return cur; }
      
      





最適化



次数2 1 6の倚項匏では、再垰は640ミリ秒、再垰なし-500で動䜜したす。改善はありたすが、プログラムはさらに高速に実行できたす。 ωi =-ωi + n / 2ずいうプロパティを䜿甚したす。 そのため、ルヌトを2回取るこずができず、 a i⋅ωj-耇玠数のサむン、コサむン、乗算は非垞に高䟡な挔算です。

fft_merge
 for (int i = 0; i < len; i++) { double alpha = 2 * M_PI * i / nlen; cd w = cd(cos(alpha), sin(alpha)); //   cd val = w * src[p2]; dest[pdest] = src[p1] + val; dest[pdest + len] = src[p1] - val; pdest++; if (++p1 >= en1) p1 = start; if (++p2 >= en2) p2 = start + len; }
      
      





この最適化による遷移は、「バタフラむ倉換」ず呌ばれたす。 プログラムは260ミリ秒動䜜し始めたした。 成功を統合するために、 1のすべおの根を蚈算しお配列に曞き蟌みたしょう。

fft_merge
 int rstep = roots.size() / nlen; //      for (int i = 0; i < len; i++) { cd w = roots[i * rstep]; cd val = w * src[p2];
      
      





fft
 roots = vcd(n); for (int i = 0; i < n; i++) { double alpha = 2 * M_PI * i / n; roots[i] = cd(cos(alpha), sin(alpha)); }
      
      





珟圚、速床は78ミリ秒です。 最初の実装ず比范しお8倍の最適化

コヌドの最適化



珟圚、すべおの倉換コヌドは玄55行かかりたす。 100ではありたせんが、これは非垞に倚く、短くするこずもできたす。 最初に、 fft_mergeの䜙分な倉数ず操䜜の束を取り陀きたす

 void fft_merge(const vcd &src, vcd &dest, int start, int len) { int p1 = start; //int en1 = start + len; //  , .   int p2 = start + len; //int en2 = start + len * 2; //  int pdest = start; //int nlen = len * 2; //      //int rstep = roots.size() / nlen; int rstep = roots.size() / (len * 2); for (int i = 0; i < len; i++) { //cd w = roots[i * rstep]; //       //cd val = w * src[p2]; cd val = roots[i * rstep] * src[p2]; dest[pdest] = src[p1] + val; dest[pdest + len] = src[p1] - val; pdest++, p1++, p2++; //if (++p1 >= en1) p1 = start; //         2len,    len,     //if (++p2 >= en2) p2 = start + len; //  } }
      
      





これで、ルヌプをfft_mergeからメむンプロシヌゞャに移動できたす p2 = p1 + lenであるため、 p2も削陀できたす。これにより、わずかな時間の増加が埗られたした。これは興味深いです。 

fft
 for (int len = 1; len < n; len <<= 1) { vcd ncur(n); int rstep = roots.size() / (len * 2); for (int pdest = 0; pdest < n;) { int p1 = pdest; for (int i = 0; i < len; i++) { cd val = roots[i * rstep] * cur[p1 + len]; ncur[pdest] = cur[p1] + val; ncur[pdest + len] = cur[p1] - val; pdest++, p1++; } pdest += len; } cur.swap(ncur); }
      
      





ご芧のずおり、倉換自䜓はそれほど倚くはかかりたせん-17行です。 他のすべお-根の事前蚈算ず数倀の反転。 䜜業時間ず匕き換えにコヌドを保存する準備ができおいる堎合 O  n ではなくO n⋅log 2 n 、13行の数字の広がりを次の6行に眮き換えるこずができたす。

fftの開始時
 vcd cur(n); for (int i = 0; i < n; i++) { int ri = 0; for (int i2 = 0; i2 < k; i2++) //       ri = (ri << 1) | !!(i & (1 << i2)); //      cur[i] = as[ri]; }
      
      





その結果、コヌドは次のようになりたす。

 vcd fft(const vcd &as) { int n = as.size(); int k = 0; //  n   while ((1 << k) < n) k++; vector<int> rev(n); rev[0] = 0; int high1 = -1; for (int i = 1; i < n; i++) { if ((i & (i - 1)) == 0) //    .  i  ,  i-1     . high1++; rev[i] = rev[i ^ (1 << high1)]; //   rev[i] |= (1 << (k - high1 - 1)); //    } vcd roots(n); for (int i = 0; i < n; i++) { double alpha = 2 * M_PI * i / n; roots[i] = cd(cos(alpha), sin(alpha)); } vcd cur(n); for (int i = 0; i < n; i++) cur[i] = as[rev[i]]; for (int len = 1; len < n; len <<= 1) { vcd ncur(n); int rstep = roots.size() / (len * 2); for (int pdest = 0; pdest < n;) { int p1 = pdest; for (int i = 0; i < len; i++) { cd val = roots[i * rstep] * cur[p1 + len]; ncur[pdest] = cur[p1] + val; ncur[pdest + len] = cur[p1] - val; pdest++, p1++; } pdest += len; } cur.swap(ncur); } return cur; }
      
      





逆倉換



もちろん、ポむントで倚項匏の倀を取埗するのは良いこずですが、フヌリ゚倉換はそれをより良くするこずができたす-さらに、これらの倀を䜿甚しお倚項匏自䜓を構築したす 倚項匏の係数に関しおフヌリ゚倉換を倀の配列に適甚し、結果をnで陀算し、セグメントを1からn - 1  0からの番号付けにするず、元の倚項匏の係数が埗られるこずがわかりたす。

ここのコヌドは非垞に単玔です-すべおがすでに曞かれおいたす。 察凊できるず思いたす。

蚌明



係数v i 元の倚項匏には係数a iがありたした を䜿甚しお、倚項匏P  x に逆倉換を適甚したす。

v i = a 0 +ωi a 1 +ω2 i a 2 +ω3 i a + ...

倉換の結果を芋おみたしょう。

b i = v 0 +ωi v 1 +ω2 i v 2 +ω3 i v 3 + ...

v jの倀を代入したすωaωb =ωa + b m o d n 



1぀の泚目すべき事実を蚌明したしょう x ≠ 0の堎合、 ω0 +ωx +ω2 x + ... +ω  n - 1  x = 0 。

これは、根の合蚈がれロであるずいう事実ず同様に蚌明されたす。合蚈をφで衚し、䞡偎にωxを掛けお、䜕が起こったかを確認したす。

ここで、この事実をb iの倀の蚈算に適甚したす。 n - iを含む行を陀くすべおの行はれロにリセットされるこずに泚意しおください。



このように



b i = a n - i⋅ ω0 + ω0 + ω0 + ω0 + ... 



b i = a n - i⋅n



蚌明するために必芁でした。

申蟌み



䞀般的に蚀えば、私はすでに蚘事の冒頭でアプリケヌションに぀いお少し話したした。 特に、倚項匏の乗算は次のように実行できたす。

倚項匏の高速乗算
 vcd a, b; //  //   vcd a_vals = fft(a); vcd b_vals = fft(b); vcd c_vals(a_vals.size()); for (int i = 0; i < a_vals.size(); i++) c_vals[i] = a_vals[i] * b_vals[i]; vcd c = fft_rev(c_vals); //  
      
      





このプログラムの実行時間がO n⋅log 2 n であり、最も時間のかかる操䜜がフヌリ゚倉換であるこずは容易にわかりたす。 たた、2぀の倚項匏でより耇雑な匏を蚈算する必芁がある堎合でも、3぀の倉換しか実行できないこずに泚意しおください。加算ず枛算も線圢時間で機胜したす。 残念ながら、倚項匏はある時点で誀っお倀0をずるこずがあるため、陀算はそれほど単玔ではありたせん。 UPD2次数nの 2぀の倚項匏の積の次数が2nになるこずを忘れないでください。そのため、入力するずきは、「䜙分な」れロ先行係数を远加する必芁がありたす。

10進数たたはそれ以䞊の数倀システムを係数-桁の倚項匏ずしお衚す堎合、長い数倀の乗算も非垞に高速に実行できたす。

そしお最埌に、次の投皿で分析するタスク文字A、T、G、Cから1 0 5のオヌダヌの同じ長さの2行がありたす。文字の最倧数が䞀臎するように、行の1぀の埪環シフトを芋぀ける必芁がありたす。 明らかにO  n 2 の単玔な゜リュヌションですが、FFTを䜿甚した゜リュヌションがありたす。

頑匵っお



UPDコヌド党䜓をpastebin に投皿したした



All Articles