FFT高速フーリエ変換の学習ノートを変換します
基本情報
用途:多項式乗算
時間の複雑さ:\(O(nlogn)\) (わずかに大きい定数)
アルゴリズムのプロセス
基本的な考え方
求\(H(x)= G(X)\回F(X)\)
直接変換係数からの発現は、第1の検討、係数比較実に式である(F(x)は、\ \ G(X)を\) 式ポイント値に、次に\(O(N)\)決定されます\(H(X)\) 、およびからの発現のポイント値(H(X)\)\にポイント値式\(H(X)\)係数発現。
前記式プロセス設定点が呼び出された係数のための表現からの変換\(DFT \)としても知られ、評価演算子。
式から式に係数セットポイントプロセスが呼び出される(IDFT \)\としても知られ、補間。
評価オペレーター
評価計算処理を考慮し、集合\(F(x)は、G (X)\) である\(N- \) 、倍(M \)\、次の多項式(\ H(X))\されます\(N + M \)次の多項式、
見つける必要が我々はそう\(F(x)は、G (X)\) で\(M-1 \ N + ) 、その最終的な決定を確実にするために、異なる点での値を(\ H(x))を\一意性、(決議の機能を必要な条件を見出すために比較することができます)。
直接ハード回数場合、複雑さが到達します\(O(N ^ 2)\) 、我々はと呼ばれるヘルプ必要があるので、単位根魔法の物事のを。
複数
単位根を導入する前に、我々は最初に説明しなければならない複合体を。
まず、我々は数を定義\(I \を)ので、(I ^ 2 = -1 \)\(全て以下、\(I \)がこの事を表明しています)。
フォーム(\ + BI)\複素数のは、呼び出される場合\(A、B \で\ R&LT mathbb {} \) 。
複素数と実数は、(実際には、操作多項式に似ている)も4つの操作があります。
设 \(x = a+bi, y=c+di\), 则
- $ X + Y =(A + C)+(B + D)私は$
- $のxy =(AC)+(BD)は、i $
- $ X \回Y =(交流 -bd)+私は$((広告+ CB) \(X、Y \)多項式乗算などを開くことができます)。
- $ \ Fracの{X} {Y} = \ FRAC {+ BI} {C +ジ} = \ FRAC {(+ BI)(C-DI)} {(C + DI)(C-DI)} = \ FRAC {(AC + BD)+(AD + CB)は、i}、{C ^ 2 + D ^ 2}(演算処理の分母で無理数と同様)$。
次に、私たちは「複素平面」と呼ばれるものものをご紹介します。
限り
数軸ポイントを一意に一意複素数を表すことができ、複素平面上の点と同様の実数を表します。
ここで、\(X \)数が実数軸である\((レアル\軸)\) 、\(Y \)数は、虚数軸である\((虚数\軸)\) 。
我々は、複雑な設定の引数に対応する複素平面内の複数の点のためのベクター及び(X \)\、角度反時計シャフト
複数のモジュール長さ、長さ複数のベクトルに対応する金型用。
私たちは、魔法の自然を取得します。
セット\(X、Y、Z \ ) された複数の、および\(X \ Y = Zタイムズ\)次に、\(Z \)ウェブ角に等しい\(X、Y \)の引数が追加され、\(Z \)ダイの長さが等しくなる\(X、Y \)ダイ長さ乗算しました。
図は、次の(図源)
引数には、金型は、カウントに直接座標を乗じて成長することができます三角証拠を要約することができますのようなものです。(筆記証拠があまりにも面倒で、私の限られた時間を許します)
単位根
上記の根拠で、我々は知って来ることができる単位根アップ。
定義:もし複数の\(N-X ^ = 1、\(N- \で\ mathbb + {N})\。)と呼ばれる、\(Xが\)である\(N- \)乗根。
、複素乗算の性質を見つけることができます考えてみましょう(X \)\ダイ長さのがバインドされている(1 \)\より大きい(、(1 \)\、その後、大規模でよりなり、以下の(1 \)\、それがよりになります)小さく取ります、
そして\(X \) 、ウェブ角度(\ \ FRAC {2 \ {N-PI K}}、\(K \ [0、N - ))\) 。
ことを意味し、\(X-は\)複素平面内になければならない単位円上の、及び単位円\(N \)十分位。
为了便于称呼, 我们用 \(\omega_n\) 来表示 \(n\) 单位根, 并从 \(1\) 开始将他们逐个编上号, \(\omega_n^0 = 1\).
接下来, 我们介绍一些单位根的性质 (原谅我真的没时间....)
- \(\omega_n^k = (\omega_n^1)^k\)
- $\omega_n^0 \omega_n^1 \dots \omega_n^{n-1} $ 互不相等.
- \(\omega_n^{k+\frac{n}{2}} = -\omega_n^k\) (\(n\) 为偶数)
- \(\omega_{2n}^{2k} = \omega_n^k\)
- \(\sum_{k=0}^{n-1} \omega_n^k = 0\) (带入等差数列求和公式即可)
好了, 复数和单位根就介绍到这里, 还记得我们原来要干什么吗?
我们想把 \(F(x)\) 从 系数表达式 转化为 点值表达式 .
求点值表达式, 就需要选择 \(n+m-1\) 个自变量 \(x\) 带入求值.
通常情况下, 这个操作的复杂度是 \(O(n^2)\) 级别的, 但我们的傅里叶大大发现, 把单位根带入求值, 会有神奇的效果.
为了方便描述, 我们这里把 \(n\) 重定义为大于 \(n+m-1\) 的第一个 \(2\) 的正数次方, 并把 \(F(x)\) 重定义为 \(n-1\) 次多项式, 后面多出的系数默认为 \(0\).
把 \(\omega_n^k\) ($ k \in [0,\frac{n}{2})$)带入 \(F(x)\), 得到
\[ F(\omega_n^k) = f[0]\omega_n^0 + f[1]\omega_n^1 + \dots + f[n-1]\omega_n^{n-1} \]
尝试使用分值的思想, 把奇偶次项分开, 得到
\[ F(\omega_n^k) = f[0]\omega_n^0 + f[2]\omega_n^2 + \dots + f[n-2]\omega_n^{n-2} + f[1]\omega_n^1 + f[3]\omega_n^3 + \dots + f[n-1]\omega_n^{n-1} \]
两部分似乎有相似之处,
设
\(G1(x) = f[0]x^0 + f[2]x^1 + f[n-2]x^{\frac{n}{2}-1}\)
\(G2(x) = f[1]x^0 + f[1]x^1 + f[n-1]x^{\frac{n}{2}-1}\)
则
\[ \begin{aligned} F(\omega_n^k) & = G1(\omega_n^{2k}) + \omega_n^kG2(\omega_n^{2k}) \\ & = G1(\omega_{\frac{n}{2}}^{k}) + \omega_n^kG2(\omega_{\frac{n}{2}}^{k}) \end{aligned} \]
若再把 \(\omega_n^{k+\frac{n}{2}}\) 带入 \(F(x)\), 由于 \(\omega_n^{k+\frac{n}{2}} = -\omega_n^k\), 所以他们的偶次项是相同的, 而奇次项是相反的.
也就是
\[ \begin{aligned} F(\omega_n^{k+\frac{n}{2}}) & = G1(\omega_n^{2k + n}) + \omega_n^{k+\frac{n}{2}}G2(\omega_n^{2k + n}) \\ & = G1(\omega_{\frac{n}{2}}^{k}) - \omega_n^kG2(\omega_{\frac{n}{2}}^{k}) \end{aligned} \]
发现 \(F(\omega_n^k)\) 和 \(F(\omega_n^k)\) 化简后得到的式子只有一个符号的差别, 那么意味着, 我们只需算出当 \(k \in [0,\frac{n}{2})\) 时的
\[ G1(\omega_{\frac{n}{2}}^{k}) \]
和
\[ G2(\omega_{\frac{n}{2}}^{k}) \]
这两个式子, 就可以算出 \(\omega_n^0\) 到 \(\omega_n^{n-1}\) 的所有点值.
而上面那两个式子显然 (应该显然吧...) 是可以递归处理的, 那么每次就减少计算一半的点, 时间复杂度就降低到了 \(O(n\log n)\).
放个代码
void trans(cn *f,int len,bool id){
if(len==1) return;
cn *g1=f,*g2=f+len/2; // 直接在 f 数组的地址上修改, 防止使用内存过多
for(int i=0;i<len;i++) tmp[i]=f[i]; // 由于是之间在 f 数组的地址上修改, 所以要备份
for(int i=0;2*i<len;i++){ g1[i]=tmp[i<<1]; g2[i]=tmp[i<<1|1]; }
trans(g1,len/2,id); // 递归处理
trans(g2,len/2,id);
cn w1=(cn){cos(2*Pi/len),sin(2*Pi/len)},wi=(cn){1,0};
if(id) w1.b*=-1;
for(int i=0;2*i<len;i++){
tmp[i]=g1[i]+wi*g2[i]; // 上面的两个式子
tmp[i+len/2]=g1[i]-wi*g2[i];
wi=wi*w1; // 处理出每个单位根
}
for(int i=0;i<len;i++) f[i]=tmp[i];
}
那么求值运算, 也就是 \(DFT\) 就大功告成了.
差值运算
我们先用矩阵乘法来表示一下求点值的过程.
设 矩阵\(A\) 为要带入的 \(n\) 个自变量以及它们的 \(0 \sim n\) 次方,
矩阵 \(B\) 为 \(F(x)\) 的系数,
矩阵 \(C\) 为自变量对应的 \(n\) 个点值.
则有
\[ AB = C \]
即
现在我们知道了 \(A\), 知道了 \(C\), 要求 \(B\), 那一般思路就是把 \(A\) 除过去, 即
\[ B = CA^{-1} \]
其中 \(A^{-1}\) 为 \(A\) 的逆矩阵, 它们的乘积为单位矩阵.
经过一系列复杂的运算后, 发现 \(A^{-1}\) 是长这样的, (可以尝试自己手推一下, 需要用到上面单位根的第 4 个性质)
是不是很眼熟,
没错, 实际上就是把 \(A\) 的 \(\omega_n^k\) 全都换成了 \(\omega_n^{-k}\), 并在前面加了个系数.
那 \(CA^{-1}\) 究竟要怎么算呢?
是不是完全没有头绪? (还是只有我一个人是这样)
答案是, 把 \(A^{-1}\) 看做 \(A\), 把 \(C\) 看做 \(B\), 把 \(B\) 看做 \(C\) , 再进行一遍 \(DFT\) 就行了. (说人话).
就是 把点值看做一个新函数的系数, 然后把 \(\omega_n^0 \sim \omega_n^{-(n-1)}\) 带入这个新函数, 求值, 得到的点值再乘上一个 \(\frac{1}{n}\) 就得到了\(H(x)\), 也就是 \(F(x) \times G(x)\) 的系数.
ok, 到此为止, 我们搞定了 \(DFT\) 和 \(IDFT\) ,\(FFT\) 的流程也就到这里了,
放代码.
#include<bits/stdc++.h>
#define _USE_MATH_DEFINES
using namespace std;
const int N=3e6+7;
const double Pi=M_PI;
struct cn{
double a,b;
cn operator + (const cn &x) const{
return (cn){x.a+a,x.b+b};
}
cn operator - (const cn &x) const{
return (cn){a-x.a,b-x.b};
}
cn operator * (const cn &x) const{
return (cn){x.a*a-x.b*b,x.a*b+a*x.b};
}
cn operator *= (const cn &x) const{
return (cn){x.a*a-x.b*b,x.a*b+a*x.b};
}
};
int n,m;
cn f[N],g[N],tmp[N];
void trans(cn *f,int len,bool id){
if(len==1) return;
cn *g1=f,*g2=f+len/2; // 直接在 f 数组的地址上修改, 防止使用内存过多
for(int i=0;i<len;i++) tmp[i]=f[i]; // 由于是之间在 f 数组的地址上修改, 所以要备份
for(int i=0;2*i<len;i++){ g1[i]=tmp[i<<1]; g2[i]=tmp[i<<1|1]; }
trans(g1,len/2,id); // 递归处理
trans(g2,len/2,id);
cn w1=(cn){cos(2*Pi/len),sin(2*Pi/len)},wi=(cn){1,0};
if(id) w1.b*=-1;
for(int i=0;2*i<len;i++){
tmp[i]=g1[i]+wi*g2[i]; // 上面的两个式子
tmp[i+len/2]=g1[i]-wi*g2[i];
wi=wi*w1; // 处理出每个单位根
}
for(int i=0;i<len;i++) f[i]=tmp[i];
}
int main(){
// freopen("FFT.in","r",stdin);
cin>>n>>m;
for(int i=0;i<=n;i++) scanf("%lf",&f[i].a);
for(int i=0;i<=m;i++) scanf("%lf",&g[i].a);
int t=1;
while(t<=n+m) t<<=1;
trans(f,t,0);
trans(g,t,0);
for(int i=0;i<t;i++) f[i]=f[i]*g[i];
trans(f,t,1);
for(int i=0;i<=n+m;i++) printf("%d ",(int)(f[i].a/t+0.49)); //+0.49 减小因精度产生的误差 (我也不知道为什么这样就可减小误差...)
return 0;
}
但是, 当你把这份代码交上去后, 会发现只有 77pts, 后面两点会 TLE.
这是因为复数运算的常数本身就比较大, 再加上递归带来的常数, 你不T谁T.
所以, 继续下一个内容.
FFT的优化
复数运算带来的常数是优化不了了, 毕竟 \(FFT\) 的关键步骤 ---- 分治 要依靠它才能进行.
(当然, 有人用其他更优的东西把它替代了, 不过这属于下一个内容 ---- \(NTT\) )
那我们就考虑如何优化递归带来的常数吧.
我们发现, 递归的下传过程并没有进行什么操作, 在上传过程中才处理出了点值.
那我们可以这样理解 : 递归的下传过程就是为了寻找每个数的对应位置.
那么, 这个对应位置是否存在某种规律, 能让我们免去递归的过程, 直接把它们放在应该放的位置?
经过前人的不懈努力和细心观察发现, 每个数最终的位置是该数的 二进制翻转
比如, 当 \(n = 8\) 的时候.
0 1 2 3 4 5 6 7
0 2 4 6 | 1 3 5 7
0 4 | 2 6 | 1 5 | 3 7
0 | 4 | 2 | 6 | 1 | 5 | 3 | 7
化为二进制就是
000 001 010 011 100 101 110 111
000 100 010 110 001 101 011 111
是不是非常神奇
然后我们可以用一个类似递归的过程来处理他们的位置
for(int i=0;i<n;i++)
num[i]=(num[i>>1]>>1])|((i&1) ?n>>1 :0)
可以这样理解,
假设你有一个数 \(x\), 它的二进制为
xxxxxxxxxx
把它拆成这两部分
xxxxxxxxx | x
前半部分的翻转, 就相当于 \(x>>1\) 的翻转再左移一位. (可以自己模拟一下)
然后再根据最后一位是 \(0\) 或 \(1\) , 在前面补上相应的一位.
ok, 这样, 我们就避免了递归带来的常数.
还有一个小地方
for(int i=0;2*i<len;i++){
tmp[i]=g1[i]+wi*g2[i]; // 上面的两个式子
tmp[i+len/2]=g1[i]-wi*g2[i];
wi=wi*w1; // 处理出每个单位根
}
我们可以把它改成
for(int i=0;2*i<len;i++){
cn tmp=wi*g2[i];
tmp[i]=g1[i]+tmp; // 上面的两个式子
tmp[i+len/2]=g1[i]-tmp;
wi=wi*w1; // 处理出每个单位根
}
减少了一下复数的运算量.
最终代码 【模板】多项式乘法(FFT)
#include<bits/stdc++.h>
#define _USE_MATH_DEFINES
using namespace std;
const int N=3e6+7;
const double Pi=M_PI;
struct cn{
double a,b;
cn operator + (const cn &x) const{
return (cn){x.a+a,x.b+b};
}
cn operator - (const cn &x) const{
return (cn){a-x.a,b-x.b};
}
cn operator * (const cn &x) const{
return (cn){x.a*a-x.b*b,x.a*b+a*x.b};
}
};
int n,m,t=1,num[N];
cn f[N],g[N],tmp[N];
void trans(cn *f,int id){
for(int i=0;i<t;i++)
if(i<num[i]) swap(f[i],f[num[i]]);
for(int len=2;len<=t;len<<=1){
int gap=len>>1;
cn w1=(cn){cos(2*Pi/len),sin(2*Pi/len)*id};
for(int i=0;i<t;i+=len){
cn wj=(cn){1,0};
for(int j=i;j<i+gap;j++){
cn tt=wj*f[j+gap];
f[j+gap]=f[j]-tt; // 这里需要注意一下赋值的顺序
f[j]=f[j]+tt;
wj=wj*w1;
}
}
}
}
int main(){
//freopen("FFT.in","r",stdin);
//freopen("x.out","w",stdout);
cin>>n>>m;
for(int i=0;i<=n;i++) scanf("%lf",&f[i].a);
for(int i=0;i<=m;i++) scanf("%lf",&g[i].a);
while(t<=n+m) t<<=1; // 保证 t > n+m
for(int i=1;i<t;i++) num[i]=(num[i>>1]>>1)|((i&1)?t>>1:0);
trans(f,1);
trans(g,1);
for(int i=0;i<t;i++) f[i]=f[i]*g[i];
trans(f,-1);
for(int i=0;i<=n+m;i++) printf("%d ",(int)(f[i].a/t+0.49));
return 0;
}
推荐题目
下面三道是 \(NTT\) 的题.
参考资料
フーリエ変換(FFT)の研究ノートを変換 command_blockことにより、
ああ、もう一つ、
とTypora素敵な