看了半天的fft,總算是看懂了?
fft(快速傅利葉變換)是用來解決多項式乘法的演算法,時間複雜度為\(o(nlog_2n)\)。
如何可以準確地表達乙個\(n\)次的多項式?一種方法是係數表示法,直接給出\(n+1\)項的係數;另一種方法是點值表示法,給出\(n+1\)個點,用這些點唯一確定乙個多項式。
在進行多項式乘法的時候,如果是用係數表示法,似乎不可避免地要用\(o(n^2)\)的時間複雜度來獲得答案;但是如果使用點值表示法,如果各個點的橫座標取值都相同,就可以\(o(n)\)實現多項式乘法(只要把縱座標相乘就可以得到所要的多項式)。
可以看出,用點值表示法可以很快地計算兩個多項式的乘積,但是問題又來了,一般情況下,我們都是用係數表示法來表示乙個多項式,係數表示法和點值表示法的轉化似乎也是要\(o(n^2)\)(\(o(n^3)\)?),有沒有一種方法可以很快地實現兩種表示方法的轉化呢?
在實數中,並不存在\(\sqrt\),擴充套件到複數集\(c\)後我們定義\(i^2=-1\),那麼\(i=\sqrt\),乙個複數\(z\)可以表示為\(a+bi\),其中\(a.b\in r\)。
可以用平面直角座標系的乙個點\((x,y)\)表示乙個複數\(z=x+yi\),準確來說,這個平面直角座標系應該就是復平面直角座標系。當然我們可以用另一種方法表示平面直角座標系上的乙個點:\((r,\theta)\),表示該點距原點的距離為\(r\),其與原點連線和\(x\)軸正方向夾角為\(\theta\)。同樣的,我們也可以用這種方法表示乙個複數。
複數的運算類似實數,把\(i\)當成乙個普通的數就好了。
複數相乘的乙個小性質:
\[(r_1,\theta_1)(r_2,\theta_2)=(r_1r_2,\theta_1+\theta_2)
\](不會證嗚嗚嗚,其實如果知道了三角函式的一些公式很容易證明,可以參考this)
dft(離散傅利葉變換)就直接跳了吧。
先假設\(n\)為2的整數次冪。
回到之前的問題,如何快速地在係數表示法和點值表示法之間進行轉換。
如果我們取\(x_0,x_1,\dots,x_\)這些數作為橫座標的話,多項式為\(f(x)=a_0x^0+a_1x^1+a_2x^2+\dots+a_x^\),那麼有:
\[\beginx_0^0&\ x_0^1&\ \cdots&\ x_0^\\ x_1^0&\ x_1^1&\ \cdots&\ x_1^\\ \vdots&\ \vdots&\ \ddots&\ \vdots \\x_^0&\ x_^1&\ \cdots&\ x_^ \end\cdot\begina_0\\a_1\\ \vdots\\a_\end=\beginy_0\\y_1\\ \vdots\\y_\end
\]fft相當於就是要快速求出這個的值,如果第乙個矩陣有逆矩陣的話,也可以通過這個式子將點值表示法轉化為係數表示法。
接下來看如何選取\(x\)的值來減小計算量,可以選取在復平面直角座標系中單位圓上的點(沒學過複數,可能表述不準確,就是那個意思),把在復平面直角座標系中單位圓平均分成\(n\)等份。
借的一張圖:
設\(w_n\)為圖中的1號點,那麼這\(n\)個等分點就分別是\(w_n^0,w_n^1,\dots,w_n^\),根據之前複數乘法的性質,可以得到:
\[w_n^k=\cos\dfrac+i\sin\dfrac
\]\(w_n\)稱作\(n\)次單位根,把這些點作為橫座標的話,可以減小計算量。
先看看單位根的幾個性質吧:
\(w_n^0=w_n^n=1,w_n^=-1,w_n^k=w_^,w_n^k=w_n^\)
直接帶入公式就可以得到了。
此時我們只需要求\(w_n\)的0到\(n-1\)次冪就可以求出所有\(x\)的冪的值了,減小了許多計算量。
然而複雜度還是\(o(n^2)\)。
怎麼辦呢,考慮分治,原多項式為:
\[f(x)=a_0x^0+a_1x^1+a_2x^2+\dots+a_x^
\]\[=(a_0x^0+a_2x^2+a_4x^4+\cdots +a_x^)+x(a_1x^0+a_3x^2+a_5x^4+\cdots +a_x^)
\]設:
\(f_1(x)=a_0x^0+a_2x^1+a_4x^2+\cdots +a_x^\frac\)
\(f_2(x)=a_1x^0+a_3x^1+a_5x^2+\cdots +a_x^\frac\)
不難發現\(f(x)=f_1(x^2)+x\cdot f_2(x^2)\)
將\(w_n^k\)帶入,得到\(f(w_n^k)=f_1(w_n^)+w_n^k\cdot f_2(w_n^)\)
又:\(w_^=w_^\)
\[\therefore f(w_n^k)=f_1(w_^k)+w_n^k\cdot f_2(w_^k)
\]看到這個方程,應該是可以直接推出遞迴是怎麼寫的了,首先拆係數,然後遞迴處理兩組係數的解,得到\(f_1(w_^)\)和\(f_2(w_^)\),根據這兩組的解得出\(f(w_n^)\)。當遞迴到\(n=1\)時,只需要求\(f(w_1^0)=a_0(w_1^0)^0=a_0\),解就是唯一的係數。
有沒有可以優化計算的地方呢?實際上是有的:
\[f(w_n^)=f_1(w_^)+w_^\cdot f_2(w_^)=f_1(w_n^k)-w_^k\cdot f_2(w_^k)
\]可以發現,它和\(f(w_n^k)\)好像差別不大,所以可以在算\(f(w_n^k)\)的時候順便把它也給算了(好像沒區別?(霧)。
然後還有乙個特殊的地方,就是最後求出來的\(y\)值可以存在和係數相同的陣列裡面,然後可以節省空間複雜度。
但是別忘了還有乙個問題,就是fft的逆運算ifft,那麼如果把單位根作為\(x\),那個矩陣有逆矩陣嗎?
相當於接下來我們是要對下面這個矩陣求逆:
\[\begin(w_n^0)^0&\ (w_n^0)^1&\ \cdots&\ (w_n^0)^\\ (w_n^1)^0&\ (w_n^1)^1&\ \cdots&\ (w_n^1)^\\ \vdots&\ \vdots&\ \ddots&\ \vdots \\(w_n^)^0&\ (w_n^)^1&\ \cdots&\ (w_^)^ \end
\]矩陣求逆後是這樣的(乾脆記住這個結論算了):
\[\begin\frac(w_n^)^0&\ \frac(w_n^)^1&\ \cdots&\ \frac(w_n^)^\\ \frac(w_n^)^0&\ \frac(w_n^)^1&\ \cdots&\ \frac(w_n^)^\\ \vdots&\ \vdots&\ \ddots&\ \vdots \\\frac(w_n^)^0&\ \frac(w_n^)^1&\ \cdots&\ \frac(w_^)^ \end
\]**大概是這樣的:
void fft(complex*a,int n,int inv)}\)。
如何證明?從低位往高位考慮的話,如果是0,就往左走,如果是1,就往右走,然後從下層往上層考慮,如果是1,貢獻為2的當前經過層數次冪,如果是0,貢獻為0;仔細想想,不就是二進位制的倒置嗎?所以可以先把每個數的最終位置算出來。
如何求二進位制倒置?如下:
for(int i=1;i<=n;++i)
rev[i]=(rev[i>>1]>>1)|((i&1)然後就可以愉快地切模板了。
void fft(complex*a,int n,int inv)
} }}
總複習小結
總複習小結 1 經過一學期的程式設計學習,初步認識基礎資料型別,語句結構,可以做一些簡單的程式設計。此次複習總結了一部分的函式 程式設計技巧及易錯知識點。2 符號常量,定義後不再被賦值,習慣上用大寫字母。例 define pi 3.14 const double pi 3.1415 定義最大值 de...
tarjan複習小結
雖然是複習,但還是學到許多。過程中遇到四種邊 1 樹枝邊 dfs 搜尋樹上的邊 滿足邊 u,v v 不在棧中 u 為 v 的父節點 2 前向邊 與 dfs 方向一致 祖先指向子孫 沒什麼用 3 後向邊 與 dfs 方向相反 子孫指向祖先 滿足邊 u,v v 在棧中,u 為 v 的祖先節點 4 橫叉邊...
Tarjan 複習小結
一 割點。void tarjan r i,r rt else low i min low i dfn to k if i rt sum 1 ans i 1 注意 在不聯通圖中,應當 for r i 1 i n i if dfn i tarjan i,i 這樣才能保證全部求到,注意根節點.二 橋。vo...