Fast Fourier Transform notes
Các bạn có thể theo dõi Youtube minh họa tuyệt vời này.
Vấn đề của bài toán nhân hai đa thức
Giả sử ta có hai đa thức \(f(x)=x+2\) và \(g(x)=x^2+3x+1\), để nhân hai đa thức này, thông thường, chúng ta phải khai triển phép nhân đa thức như sau:
$$\begin{align*} f(x)g(x)&=(x+2)(x^2+3x+1) \\ &=x(x^2+3x+1) + 2(x^2+3x+1) ()\\ &=(x^3+3x^2+x) + (2x^2+6x+2)\\ &=x^3 + 5x^2 + 7x + 2 \end{align*}$$
Để ý rằng, việc khai triển chính là lấy từng hệ số của đa thức \(f(x)\) rồi nhân với toàn bộ các hệ số của đa thức \(g(x)\). Việc nhân đa thức như vậy sẽ làm cho độ phức tạp của bài toán nhân đa thức trở thành \(O(n^2)\).
Fast Fourier Transform là một phương pháp biến đổi nhanh một đa thức thành một tập hợp các điểm thuộc đa thức ấy, cho phép ta giảm độ phức tạp của bài toán nhân đa thức trở thành \(O(n\log n)\). Bây giờ chúng ta cùng xem xét từng bước của thuật toán này!
Xác định đa thức thông qua các điểm
Định lý: Có duy nhất một đa thức bậc \(k\) đi qua \(k+1\) điểm cho trước.
Mình sẽ không chứng minh định lý này vì các bạn có thể tìm thấy các chứng minh này dễ dàng với từ khoá “nội suy Lagrange“, chỉ xin đưa vài ví dụ để bạn dễ hiểu:
Có lẽ ai cũng biết đến tiên đề Euclide: “qua hai điểm cho trước trên mặt phẳng, chỉ có thể kẻ một và chỉ một đường thẳng qua hai điểm đã cho“. Chúng ta cũng biết rằng phương trình đường thẳng là một đa thức bậc một có dạng \(y=f(x)=ax+b\), rõ ràng, chỉ cần hai điểm là ta xác định được phương trình đường thẳng này.
Mở rộng hơn, nếu ta có 3 điểm: \((0,1),(-1,-1),(1,5)\)thì đa thức bậc 2 dạng \(f(x)=ax^2+bx+c\) duy nhất đi qua ba điểm đó là \(f(x)=x^2+3x+1\).
Tổng quát, nếu ta có đa thức dạng \(f(x) = a_kx^k + a_{k-1}x^{k-1}+\dots+a_1x+a_0\) ,và \(k+1\) điểm phân biệt:\((x_0,y_0),(x_1,y_1),\dots,(x_k,y_k)\). Để xác định \(f(x)\) ta cần giải hệ phương trình sau:
$$\begin{cases} a_kx_0^k + a_{k-1}x_0^{k-1}+\dots+a_1x_0+a_0 &= y_0\\ a_kx_1^k + a_{k-1}x_1^{k-1}+\dots+a_1x_1+a_0 &= y_1\\ \dots \\ a_kx_k^k + a_{k-1}x_k^{k-1}+\dots+a_1x_k+a_0 &= y_k \end{cases}$$
Hệ phương trình trên có thể viết lại dưới dạng ma trận như sau:
$$\underbrace{ \begin{bmatrix} 1 & x_0 & \cdots & x_0^k \\ 1 & x_1 & \cdots & x_1^k \\ \vdots & \vdots & \ddots & \vdots \\ 1 & x_k & \cdots & x_k^k \end{bmatrix}}{X} \times \underbrace{ \begin{bmatrix} a_0\\ a_1\\ \vdots \\ a_k \end{bmatrix}}{A} = \underbrace{ \begin{bmatrix} y_0\\ y_1\\ \vdots \\ y_k \end{bmatrix}}_{Y}$$
Suy ra: \(A = X^{-1}Y\).
Nghĩa là thay vì ta tính trực tiếp \(p(x)=f(x)g(x)\) bằng cách khai triển như trên, thay vào đó, ta sẽ đi tính \(k+1\) điểm với \(k\) là bậc của \(p(x)\) rồi từ đó tìm ngược lại các hệ số của \(p(x)\). Nhưng tại sao lại phải rườm rà như vậy? Đó là vì chúng ta có cách tiết kiệm tính toán khi phải tính giá trị của các điểm thuộc đa thức \(f(x), g(x)\).
Đa thức bậc chẵn và đa thức bậc lẻ
Đa thức bậc chẵn là đa thức chỉ chứa giá trị biến \(x\) ở bậc chẵn, ngược lại đa thức bậc lẻ là đa thức chỉ chứa giá trị biến \(x\) ở bậc lẻ. Ví dụ: \(f(x)=x^4+3x^2+5\) là đa thức bậc chẵn, ngược lại \(g(x) = -x^3 + 2x\) là đa thức bậc lẻ.
Dễ dàng thấy đối với đa thức bậc chẵn ta luôn có \(f(x)=f(-x)\) và với đa thức bậc lẻ thì \(f(x) = -f(-x)\). Điều này giúp chúng ta tiết kiệm khá nhiều tính toán, vì chỉ cần tính \(f(x)\) ta có thể suy ra ngay giá trị của \(f(-x)\) mà ko cần tính lại. Ví dụ: \(f(x)=x^2+1\) thì \(f(1) = 2\) có thể suy ra ngay \(f(-1)=2\) mà không cần tính toán.
Một nhận xét nữa là mọi đa thức đều có thể phân tích thành dạng tổng của một đa thức bậc chẵn và một đa thức bậc lẻ. Ví dụ: \(f(x) = 3x^6-4x^5+5x^2+2x-7\) có thể tách thành hai đa thức, một bậc chẵn\(f_e(x) = 3x^6+5x^2-7\) và một bậc lẻ \(f_o(x)=-4x^5+2x\). Đa thức bậc lẻ \(f_o(x)\) lại có thể biểu diễn thành một đa thức bậc chẵn bằng cách tách biến \(x\) ra làm thừa số chung, ta có\(f_o(x)=-4x^5+2x = x(-4x^4+2)\), rõ ràng \(-4x^4+2\) là đa thức bậc chẵn.
Ví dụ: Đa thức\(f(x) = 3x^6-4x^5+5x^2+2x-7\), ta biến đổi như sau:
$$\begin{align*} f(x)&=3x^6-4x^5+5x^2+2x-7\\ &=(3x^6+5x^2-7) + (-4x^5+2x) \\ &=\underbrace{((3x^2)^3+5x^2-7)}{f_1(x^2)} + x\underbrace{(-4(x^2)^2+2)}{f_2(x^2)} \\ \implies f(x)&=f_1(x^2)+xf_2(x^2) \\ \implies f(-x) &= f_1(x^2) - xf_2(x^2) \end{align*}$$
Có thể thấy rằng quá trình tính toán \(f(x)\) và \(f(-x)\) có sự trùng lặp các thành phần \(f_1(x^2)\) và \(f_2(x^2)\) và từ đó có thể sử dụng lại kết quả mà không cần tính lại. Nếu xem \(x^2\) là một biến \(t\) thì \(f_1(x^2) \) trở thành một đa thức \(g\) theo biến \(t\) và từ đó lại tiếp tục có thể áp dụng công thức như trên \(g(t)=g_1(t^2) +t.g_2(t^2)\) và cứ tiếp diễn như vậy… Như vậy, nếu có một dãy các giá trị \(x_1,x_2,\dots,x_n\) thỏa \(x_{k+1} = x_k^2\) thì việc tính \(f(x_1)\) sẽ được tính thông qua \(f(x_2)\), \(f(x_2)\) lại được tính thông qua \(f(x_3)\)… và sẽ trở thành một bài toán toán đệ quy như hình dưới:
Như vậy đối với đa thức bậc \(n\), ta cần tính \(\log n\) tầng đệ quy, từ đó, độ phức tạp sẽ là \(O(\log n)\) để tính giá trị cho đa thức tại một giá trị của \(x\). Chúng ta cần có \(n+1\) giá trị \(x\) để có thể nội suy, từ đó ta có thể kết luận độ phức tạp thuật toán là \(O(n\log n)\).
Căn đơn vị
Nhắc lại phương trình Euler vĩ đại: \(\boxed{\displaystyle e^{i\alpha} = \cos \alpha + i\sin \alpha} \)
Thuật toán Fast Fourier Transform cho đa thức \(f\) với bậc \(k\) để tính toán giá trị tại \(n\) điểm với \(n\) là số tự nhiên nhỏ nhất thỏa \(n \ge 2^{\log k}\). Để tiện lợi, ta đặt căn đơn vị \(w^n = 1 = e^{\frac {2 \pi i} n}\) và tính giá trị của đa thức tại \(n\) điểm tương ứng \(w^0, w^1,\dots,w^{n-1}\).
Thuật toán FFT
Thuật toán được phát biểu đệ quy theo chiều sâu (Depth First Search) qua function \(fft(f)\) như sau:
Tính \(n\) là số tự nhiên nhỏ nhất sao cho \(2^{\log k} \le n\)
Tách đa thức \(f\) thành hai đa thức \(f_e\) với các bậc chẵn và \(f_o\) với các bậc lẻ
Tính đệ quy \(y_e = fft(f_e)\) và \(y_o=fft(f_o)\)
Tính giá trị khởi tạo cho chuỗi căn đơn vị \(w = e^{\frac {2\pi i} n}\)
Cho \(\displaystyle i: 1 \to n\) và \(y\) là mảng trung gian chứa \(n\) phần tử
Tính \(y[i] = y_e[i]+w^iy_o[i]\) ứng với bước \(f(x)=f_1(x^2)+xf_2(x^2)\)
Tính \(y[i + \frac n 2] = y_e[i]-w^iy_o[i]\) ứng với bước \(f(-x)=f_1(x^2)-xf_2(x^2)\)
Return \(y\)
Dưới đây là code minh họa bằng javascript
function fft(p){
if (p.length == 1) {
return p;
}
// find the smallest power of 2 that is greater than or equal to the length of the polynomial
const n = 2**Math.ceil(Math.log2(p.length));
// calculate the nth root of unity via Euler's formula
const re = Math.cos(-2 * Math.PI / n);
const im = Math.sin(-2 * Math.PI / n);
let w = new ComplexNumber(re, im);
// pad the polynomial with zeros to make its length a power of 2
const p_ = p.concat(Array(n - p.length).fill(0));
const p_even = p_.filter((_, i) => i % 2 == 0);
const p_odd = p_.filter((_, i) => i % 2 == 1);
const y_even = fft(p_even);
const y_odd = fft(p_odd);
// combine the results of the subproblems
const y = Array(n);
let wi = new ComplexNumber(1, 0);
for (let i = 0; i < n / 2; i++) {
// combine the results of the subproblems
// y[i] = y_even[i] + w^i * y_odd[i]
y[i] = y_even[i].add(wi.mul(y_odd[i]));
// y[i + n/2] = y_even[i] - w^i * y_odd[i]
y[i + n / 2] = y_even[i].sub(wi.mul(y_odd[i]));
// update the nth root of unity w^i
wi = wi.mul(w);
}
return y;
}
Nội suy từ tập hợp điểm thành đa thức
Phần bên trên, chúng ta đã giải quyết xong vấn đề tính toán tập hợp điểm. Vấn đề còn lại là làm cách nào nội suy từ tập điểm này trở thành đa thức.
Như đã đề cập ở phần trên, ta có:
$$\underbrace{ \begin{bmatrix} 1 & x_0 & \cdots & x_0^k \\ 1 & x_1 & \cdots & x_1^k \\ \vdots & \vdots & \ddots & \vdots \\ 1 & x_k & \cdots & x_k^k \end{bmatrix}}{X} \times \underbrace{ \begin{bmatrix} a_0\\ a_1\\ \vdots \\ a_k \end{bmatrix}}{A} = \underbrace{ \begin{bmatrix} y_0\\ y_1\\ \vdots \\ y_k \end{bmatrix}}_{Y}$$
Ta có \(X \times A = Y\). Hiện tại chúng ta có tập hợp điểm và muốn xác định đa thức, nghĩa là tính \(A\), vậy \(A=X^{-1}Y\), nghĩa là chúng ta phải tính được \(X^{-1}\). May mắn thay, vì cách chọn chuỗi căn đơn vị với \(w=e^{\frac {2 \pi i} n}\) làm tập hợp các điểm cần tính nên ta có:
$$X = \begin{bmatrix} 1 & 1 & \dots& 1\\ 1 & w & \dots & w^{n-1} \\ 1 & w^2 & \dots & w^{2(n-1)} \\ \vdots & \vdots & \ddots & \vdots\\ 1 & w^{n-1} & \dots& w^{(n-1)(n-1)} \end{bmatrix}$$
Ma trận \(X\) có tính chất là mà trận nghịch đảo của nó cũng chính là ma trận chứa các thành phần liên hợp của các phần tử của \(X\)
$$X^{-1} = \frac 1 n \begin{bmatrix} 1 & 1 & \dots& 1\\ 1 & w^{-1} & \dots & w^{-(n-1)} \\ 1 & w^{-2} & \dots & w^{-2(n-1)} \\ \vdots & \vdots & \ddots & \vdots\\ 1 & w^{-(n-1)} & \dots& w^{(n-1)(n-1)} \end{bmatrix}$$
Từ đó, có thể tính nghịch đảo của FFT hay còn gọi là nội suy từ FFT một cách nhanh chóng.
Source code
// inverse FFT
function ifft(y){
const n = y.length;
const y_ = y.map(c => c.conj());
const p_ = fft(y_).map(c => c.div(n));
return p_;
}