Bài viết này giới thiệu một thuật toán có thể tính tích của hai đa thức bậc \(n\) trong thời gian \(O(n\log n)\), hiệu quả hơn thuật toán ngây thơ \(O(n^2)\). Vì phép nhân hai số nguyên cũng có thể xem như nhân hai đa thức, nên thuật toán này cũng dùng để tăng tốc nhân số nguyên lớn.
Mở đầu
Xét hai đa thức \(A\) và \(B\):
\[
\begin{aligned}
A ={}& 5x^2 + 3x + 7 \\
B ={}& 7x^2 + 2x + 1 \\
\end{aligned}
\]
Tích \(C = A \times B\) có thể tính trong \(O(n^2)\) (với \(n\) là bậc của \(A\) hoặc \(B\)):
\[
\begin{aligned}
C ={}& A \times B \\
={}& 35x^4 + 31x^3 + 60x^2 + 17x + 7
\end{aligned}
\]
Rõ ràng hệ số \(c_i\) của \(C\) thỏa \(c_i = \sum_{j = 0}^i a_j b_{i - j}\). Với thuật toán ngây thơ, mỗi hệ số tốn \(O(n)\), có \(O(n)\) hệ số nên tổng là \(O(n^2)\).
Có thể tăng tốc không? Nếu dùng FFT, ta giảm xuống \(O(n\log n)\).
Biến đổi Fourier
Biến đổi Fourier (Fourier Transform) là phương pháp phân tích tín hiệu: phân rã tín hiệu thành các thành phần hoặc tổng hợp tín hiệu từ các thành phần. Nhiều dạng sóng có thể làm thành phần; biến đổi Fourier dùng sóng sin làm thành phần.
Cho \(f(t)\) theo thời gian \(t\), biến đổi Fourier đo mức độ xuất hiện của tần số \(\omega\) trong \(f(t)\):
Dạng của biến đổi ngược rất giống biến đổi thuận; mẫu \(2\pi\) đúng bằng chu kỳ của hàm mũ phức.
Biến đổi Fourier tương đương lấy tích vô hướng liên tục giữa hàm miền thời gian và hàm mũ phức chu kỳ \(2\pi\). Biến đổi ngược cũng là một tích vô hướng.
Biến đổi Fourier có định lý chập, chuyển chập miền thời gian thành tích miền tần số, và ngược lại.
Biến đổi Fourier rời rạc
Biến đổi Fourier rời rạc (Discrete Fourier Transform, DFT) là dạng rời rạc theo cả thời gian và tần số, biến mẫu thời gian thành mẫu của DTFT (discrete-time Fourier transform) trong miền tần số.
Biến đổi Fourier là tích vô hướng dạng tích phân; DFT là tích vô hướng dạng tổng.
Cho dãy \(\{x_n\}_{n=0}^{N-1}\) thỏa điều kiện hữu hạn, DFT là:
Hệ số chuẩn hóa ở DFT/IDFT không quá quan trọng. Ở định nghĩa trên, DFT có hệ số \(1\), IDFT có hệ số \(\frac{1}{N}\). Đôi khi cả hai cùng lấy \(\frac{1}{\sqrt{N}}\).
DFT vẫn là biến đổi từ miền thời gian sang miền tần số. Vì là dạng tổng nên có cách diễn giải khác.
Nếu xem dãy \(x_n\) là hệ số của đa thức \(f(x)\) tại \(x^n\), thì \(X_k\) chính là giá trị \(f(\mathrm{e}^{\frac{-2\pi \mathrm{i}k}{N}})\) tại các căn đơn vị.
Vì vậy DFT là phép đánh giá đa thức tại các căn đơn vị.
với \(\alpha = \mathrm{e}^{-\mathrm{i}\frac{2\pi}{N}}\).
Biến đổi Fourier nhanh
FFT (Fast Fourier Transform) là thuật toán tính DFT hiệu quả. Nó không phát hiện lý thuyết mới, nhưng là bước tiến lớn trong ứng dụng DFT trên máy tính. NTT (Number Theoretic Transform) là FFT trên nền số học.
Năm 1965, Cooley và Tukey công bố FFT. Thực ra FFT đã được biết trước đó, nhưng khi ấy máy tính hiện đại chưa ra đời nên tầm quan trọng không được nhận ra. Có ý kiến cho rằng Runge và König phát hiện năm 1924, nhưng Gauss đã có thuật toán từ năm 1805 mà chưa công bố.
Cài đặt chia để trị
Ý tưởng cơ bản của FFT là chia để trị. Với DFT, ta chia để trị theo đánh giá tại \(x=\omega_n^k\). Với FFT cơ số 2, chia đa thức theo hạng chẵn và hạng lẻ.
Vì vậy, khi có \(G(\omega_{n/2}^k)\) và \(H(\omega_{n/2}^k)\), ta đồng thời có \(f(\omega_n^k)\) và \(f(\omega_n^{k+n/2})\). Đệ quy DFT cho \(G\) và \(H\) là xong.
Do DFT chia để trị yêu cầu độ dài là \(2^m (m\in\mathbf{N}^\ast)\), nên trước khi DFT phải đệm \(0\) để độ dài thành lũy thừa của 2, bậc cao nhất là \(2^m-1\).
Khi đánh giá, ta dùng \(n\) điểm \(\omega_n^0,\omega_n^1,\ldots,\omega_n^{n-1}\) với \(n=2^m\).
Về cài đặt, STL có complex và exp để lấy \(\omega_n\); hoặc dùng công thức Euler để tính số phức cũng tương đương.
Đây là phần DFT của FFT: chuyển đa thức từ biểu diễn hệ số sang biểu diễn giá trị.
Lưu ý: vì là căn đơn vị phức, cần đệm lên \(n=2^k\).
#include<cmath>#include<complex>usingComp=std::complex<double>;// complex của STLconstexprCompI(0,1);// iconstexprintMAX_N=1<<20;Comptmp[MAX_N];// rev=1, DFT; rev=-1, IDFT// Sau khi gọi hàm cần lưu ý xử lý hệ số chuẩn hóavoidDFT(Comp*f,intn,intrev){if(n==1)return;for(inti=0;i<n;++i)tmp[i]=f[i];// Chẵn sang trái, lẻ sang phảifor(inti=0;i<n;++i){if(i&1)f[n/2+i/2]=tmp[i];elsef[i/2]=tmp[i];}Comp*g=f,*h=f+n/2;// Đệ quy DFTDFT(g,n/2,rev),DFT(h,n/2,rev);// cur là căn đơn vị hiện tại; với k=0 thì omega^0_n = 1// step là bước giữa hai căn đơn vị: omega^k_n = step*omega^{k-1}_n// Tương đương exp(I*(-2*M_PI/n*rev))Compcur(1,0),step(cos(2*M_PI/n),sin(-2*M_PI*rev/n));for(intk=0;k<n/2;++k){// F(omega^k_n) = G(omega^k_{n/2}) + omega^k_n*H(omega^k_{n/2})tmp[k]=g[k]+cur*h[k];// F(omega^{k+n/2}_n) = G(omega^k_{n/2}) - omega^k_n*H(omega^k_{n/2})tmp[k+n/2]=g[k]-cur*h[k];cur*=step;}for(inti=0;i<n;++i)f[i]=tmp[i];}
Độ phức tạp \(O(n\log n)\).
Cài đặt nhân đôi
Thuật toán có thể tối ưu theo hướng “giả lập đệ quy”. Với FFT cơ số 2, ta tách chẵn/lẻ đến khi còn một hệ số, nhưng đệ quy tốn bộ nhớ. Thay vào đó, có thể “tách” trong mảng bằng hoán vị đảo bit, rồi “nhân đôi” để trộn kết quả.
“Trộn” dùng phép biến đổi bướm để chỉ cần \(O(1)\) bộ nhớ phụ.
Hoán vị đảo bit
Ví dụ đa thức 8 hạng:
Ban đầu: \(\{x_0, x_1, x_2, x_3, x_4, x_5, x_6, x_7\}\)
Sau 1 lần chia: \(\{x_0, x_2, x_4, x_6\},\{x_1, x_3, x_5, x_7 \}\)
Sau 2 lần: \(\{x_0,x_4\} \{x_2, x_6\},\{x_1, x_5\},\{x_3, x_7 \}\)
Sau 3 lần: \(\{x_0\}\{x_4\}\{x_2\}\{x_6\}\{x_1\}\{x_5\}\{x_3\}\{x_7 \}\)
Quy luật: viết chỉ số ở nhị phân rồi đảo bit, đó là vị trí mới. Ví dụ \(x_1\) là 001, đảo thành 100 tức 4. Biến đổi này gọi là hoán vị đảo bit (bit-reversal permutation).
Có thể tính \(O(n)\):
Hoán vị đảo bit (\(O(n)\))
1 2 3 4 5 6 7 8 9101112131415161718192021222324
/* * Biến đổi đảo bit trước FFT và IFFT * Vị trí i và vị trí đảo bit của i hoán đổi * len phải là lũy thừa của 2 */voidchange(Complexy[],intlen){// Ban đầu i là 0...01, j là 10...0, đối xứng nhau trong nhị phân// i tăng dần, j luôn đối xứng với i, đến khi i = 1...11for(inti=1,j=len/2,k;i<len-1;i++){// Hoán đổi cặp đảo bit; i < j đảm bảo chỉ đổi một lầnif(i<j)swap(y[i],y[j]);// i tăng bình thường, j tăng theo quy tắc đảo bit để luôn đối xứng// k là bit cao nhất đang là 0. j trừ dần phần toàn 1 ở cao vị// đến khi gặp 0 thì cộng lại// Số lần lật bit: bit cao nhất lật n lần, tiếp theo lật n/2 lần, ...// T(n) = n + n/2 + n/4 + ... = O(n)k=len/2;while(j>=k){j=j-k;k=k/2;}j+=k;}}
Cũng có thể tính từ nhỏ đến lớn. Với \(len=2^k\), ký hiệu \(R(x)\) là số đảo bit của \(x\) (đệm 0 ở đầu). Ta cần \(R(0),\ldots,R(n-1)\).
\(R(0)=0\). Khi tính \(R(x)\), ta đã biết \(R(\lfloor x/2 \rfloor)\). Dịch phải 1 bit rồi đảo, rồi lại dịch phải 1 bit sẽ cho phần đảo của tất cả bit trừ bit thấp nhất.
Nếu bit thấp nhất là 0 thì bit cao nhất sau đảo là 0; nếu là 1 thì bit cao nhất là 1, nên cộng thêm \(\frac{len}{2}=2^{k-1}\).
Xét \((1100)_2\), ta có \(R((1100)_2)=R((01100)_2)=(00110)_2\), dịch phải 1 được \((00011)_2\).
Xét bit thấp nhất: nếu là 1 thì cộng \((10000)_2=2^{k-1}\); nếu là 0 thì không cộng.
Hoán vị đảo bit (\(O(n)\))
1 2 3 4 5 6 7 8 910111213141516
// Vẫn cần len là lũy thừa của 2// rev[i] là giá trị đảo bit của ivoidchange(Complexy[],intlen){for(inti=0;i<len;++i){rev[i]=rev[i>>1]>>1;if(i&1){// Nếu bit cuối là 1 thì đảo thành len/2rev[i]|=len>>1;}}for(inti=0;i<len;++i){if(i<rev[i]){// Mỗi cặp chỉ hoán đổi một lầnswap(y[i],y[rev[i]]);}}return;}
Tối ưu phép biến đổi bướm
Biết \(G(\omega_{n/2}^k)\) và \(H(\omega_{n/2}^k)\), ta cần:
Muốn khôi phục hệ số, nhân hai vế với nghịch đảo của ma trận giữa. Ma trận này có cấu trúc đặc biệt: chỉ cần lấy nghịch đảo từng phần tử, rồi chia cho \(n\).
Lưu ý: độ dài FFT không phải độ dài đa thức; độ dài biến đổi phải đủ lớn cho tích, nên cần đệm \(0\) ở bậc cao.
Do đó ta có thể dùng căn đơn vị đảo: lấy \(\omega_k=\mathrm{e}^{-\frac{2\pi \mathrm{i}}{k}}\), rồi chia cho \(n\). Ta có thể truyền tham số \(1\) hoặc \(-1\) để nhân vào \(\pi\): \(1\) là DFT, \(-1\) là IDFT.
Tính chu kỳ của căn đơn vị
Cũng có thể hiểu IDFT qua tính chu kỳ của căn đơn vị.
Gọi \(f(x)=\sum_{i=0}^{n-1}a_ix^i\). IDFT biến từ giá trị tại căn đơn vị về hệ số.
Dùng phương pháp xây dựng. Biết \(y_i=f(\omega_n^i)\), cần tìm \(\{a_0,\ldots,a_{n-1}\}\). Đặt
Tức lấy căn đơn vị đảo, chạy FFT cho \(\{y_i\}\) rồi chia \(n\) là ra hệ số.
Cách 2
Trực tiếp thay \(\omega_n^i\) vào \(A(x)\).
Suy luận tương tự, có \(A(\omega_n^k)=\sum_{j=0}^{n-1}a_jS(\omega_n^{j+k})\). Chỉ khi \(j+k=0\pmod{n}\) thì \(S(\omega_n^{j+k})=n\), nên \(A(\omega_n^k)=a_{n-k}\cdot n\).
Tức DFT trên \(\{y_i\}\), chia \(n\), rồi đảo \(n-1\) phần tử là thu được hệ số.
/* * Thực hiện FFT * len phải là dạng 2^k * on == 1 là DFT, on == -1 là IDFT */voidfft(Complexy[],intlen,inton){// Hoán vị đảo bitchange(y,len);// Mô phỏng hợp nhất: từ độ dài 1 lên 2, rồi lên 4, ... tới lenfor(inth=2;h<=len;h<<=1){// wn: khoảng giữa các căn đơn vị hiện tại: w^1_hComplexwn(cos(2*PI/h),sin(on*2*PI/h));// Hợp nhất, có len / h lầnfor(intj=0;j<len;j+=h){// w bắt đầu từ 1 = w^0_n, rồi tăng theo wn: w^1_n, ...Complexw(1,0);for(intk=j;k<j+h/2;k++){// Nửa trái và nửa phải là nghiệm của bài toán conComplexu=y[k];Complext=w*y[k+h/2];// Gộp hai nửay[k]=u+t;y[k+h/2]=u-t;// Nửa sau của “step” luôn đối xứng âm với nửa trước// Quay một vòng là trở lại, quay nửa vòng là đổi dấu// Bình phương của số và của đối nó là bằng nhauw=w*wn;}}}// Nếu là IDFT, còn phải chia mỗi phần tử cho lenif(on==-1){for(inti=0;i<len;i++){y[i].x/=len;y[i].y/=len;}}}
/* * Thực hiện FFT * len phải là dạng 2^k * on == 1 là DFT, on == -1 là IDFT */voidfft(Complexy[],intlen,inton){change(y,len);for(inth=2;h<=len;h<<=1){// Mô phỏng hợp nhấtComplexwn(cos(2*PI/h),sin(2*PI/h));// Tính căn đơn vị hiện tạifor(intj=0;j<len;j+=h){Complexw(1,0);// Tính căn đơn vị hiện tạifor(intk=j;k<j+h/2;k++){Complexu=y[k];Complext=w*y[k+h/2];y[k]=u+t;// Gộp hai nửay[k+h/2]=u-t;// Nửa sau của “step” luôn đối xứng âm với nửa trước// Quay một vòng là trở lại, quay nửa vòng là đổi dấu// Bình phương của số và của đối nó là bằng nhauw=w*wn;}}}if(on==-1){reverse(y+1,y+len);for(inti=0;i<len;i++){y[i].x/=len;y[i].y/=len;}}}
Last updated on this page:, Update history Found an error? Want to help improve? Edit this page on GitHub! Contributors to this page:H-J-Granger, ranwen, abc1763613206, Ahacad, Allenyou1126, AndrewWayne, AngelKitty, AtomAlpaca, Backl1ght, billchenchina, c-forrest, CCXXXI, Cheuring, Chrogeek, ChungZH, countercurrent-time, DepletedPrism, Early0v0, EarthMessenger, Enter-tainer, F1shAndCat, GavinZhengOI, Gesrua, Great-designer, greyqz, Haohu Shen, henryrabbit, heroming, hly1204, Ir1d, isdanni, jiang1997, kenlig, Lewy Zeng, lucifer1004, Menci, muoshuosha, NachtgeistW, needtocalmdown, opsiff, ouuan, ouuan, partychicken, schtonn, Sshwy, sshwy, StudyingFather, SukkaW, Taoran-01, Tiphereth-A, TrisolarisHD, untitledunrevised, Xeonacid, YouXam, Yukimaikoriya All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply