Professional Documents
Culture Documents
Table of Contents
Trong bài này chúng ta sẽ tìm hiểu về thuật toán nhân nhanh hai đa thức sử dụng phép biến đổi
Fourier nhanh (Fast Fourier Transform - FFT) và cách cài đặt của nó. Bài viết này sẽ chỉ nêu chứng minh
sơ lược của một vài tính chất được sử dụng. Các chứng minh chi tiết có thể tìm thấy ở mục tài liệu
tham khảo phía cuối của bài viết.
Trong khi phiên bản nguyên thủy có lịch sử hoành tráng như vậy, phép biến đổi Fourier nhanh, dù
được cho là ra đời trước, lại được quan tâm chậm hơn nhiều. Người ta cho rằng những ý tưởng đầu
tiên về biến đổi Fourier nhanh được phát triển bởi nhà toán học Đức Carl Friedrich Gauss (1777 -
1855) vào năm 1805 khi ông cố gắng xác định quỹ đạo của các thiên thạch [3], nhưng khi đó ông
không công bố kết quả của mình. Mối liên hệ giữa Gauss và phép biến đổi Fourier nhanh chỉ được
phát hiện khi các công trình của ông được tập hợp và công bố vào năm 1866. Mặc dù vậy, vào thời đó
không có ai quan tâm tới công trình này vì lý thuyết độ phức tạp tính toán chưa phát triển (mãi tới
năm 1936 Alan Turing mới phát triển mô hình tính toán đầu tiên, và phải tới năm 1965 thì lịch sử
ngành nghiên cứu về độ phức tạp tính toán mới bắt đầu với công trình của Hartmanis và Stearns [4]).
Cũng trong năm 1965 hai nhà toán học trong ban cố vấn khoa học của tổng thống Mỹ Kennedy là
James Cooley và John Tukey đã tự tìm ra phép biến đổi nhanh Fourier trong khi thiết kế hệ thống phát
hiện các vụ thử hạt nhân của chính quyền Xô Viết [3]. Kể từ thời điểm đó phép biến đổi nhanh Fourier
mới chính thức được quan tâm và nghiên cứu ứng dụng trong rất nhiều các lĩnh vực nghiên cứu khác
nhau của vật lý, sinh học, điện tử, y tế, điều khiển học…
Nghiên cứu chỉ ra rằng mắt và tai người, động vật có "cài đặt" sẵn thuật toán biến đổi Fourier để giúp
chúng ta nhìn và nghe, vì vậy nó được GS Ronald Coifman của đại học Yale gọi là Phương pháp phân
tích dữ liệu của tự nhiên ("Nature's way of analyzing data") [1].
2 d 2 e
p(x) = a0 + a1 x + a2 x +. . . +ad x q(x) = b0 + b1 x + b2 x +. . . +be x
trong đó
c j = ∑ ai bj−i j = 0, 1, . . . , d + e
i=0
Cách làm theo định nghĩa là ta nhân mỗi hệ số của p(x) với tất cả các hệ số của q(x) rồi cộng các hệ
số của cùng tổng số mũ. Vì hai đa thức có d + 1 và e + 1 hệ số nên cách làm này có độ phức tạp là
O((d + 1)(e + 1)) = O(de) . Khi d và e tương đối lớn cỡ 103 hoặc 104 trở lên thì độ phức tạp này là
quá lớn để chạy trên máy tính, đặc biệt là các máy tính nhúng đòi hỏi tốc độ tính toán nhanh. Phép
biến đổi FFT giúp thực hiện phép nhân nói trên trong độ phức tạp O(N ∗ logN ) trong đó N là lũy
thừa của 2 nhỏ nhất lớn hơn d và e .
1 2 n−1
1 z z … z a0
⎡ 0 0 0 ⎤⎡ ⎤
⎢ 1 2 n−1 ⎥ a1
z1 z … z ⎢ ⎥
⎢ 1 1 ⎥ ⎢ ⎥
⎢ ⎥
⎢ ⎥ ⎢ ⎥
⎢ … … … … … ⎥ ⎢ ⋮ ⎥
⎣ 1 2 n−1 ⎦⎣ ⎦
1 z z … z an−1
n−1 n−1 n−1
p(z0 )
⎡ ⎤
⎢ p(z1 ) ⎥
⎢ ⎥
⎢ ⎥ (1)
⎢ ⎥
⎢ ⋮ ⎥
⎣ ⎦
p(zn−1 )
Ma trận vuông V kích cỡ n ∗ n của z0:n−1 ở trên được gọi là ma trận Vandermonde. Ta có các định lý
sau:
Chứng minh (sơ lược): Với mỗi hàng i = 0, 1, … n − 2 của định thức ta liên tục thay hàng
j = i + 1, i + 2, … n − 1 bằng hiệu của các hệ số của hàng j trừ đi hàng i. Đây là phép biến đổi cơ
bản (elementary operation) nên giá trị định thức cần tính không đổi. Lấy nhân tử chung zj − zi ở tất cả
các hàng ra ngoài và xét tiếp hàng i + 1. Sau khi xét xong i = n − 2 ta được một ma trận chéo có
đường chéo chỉ gồm zii = 1 , định thức của ma trận này hiển nhiên bằng 1. Vì vậy định thức cần tính
là tích của tất cả các nhân tử chung bỏ ra ngoài ở các bước trước đó.
Phép chứng minh bằng quy nạp có thể xem thêm tại đây
Định lý 2: Đa thức p(x) được xác định duy nhất bởi các giá trị của nó p(z0 ), p(z1 ), … p(zn−1 ) khi n
giá trị z0 , z1 , … zn−1 phân biệt. Ta gọi đây là phép biến đổi ngược.
Chứng minh:
Coi phương trình (1) là một hệ phương trình n ẩn với bộ nghiệm a0 , a1 , … an−1 . Để đa thức p(x)
xác định và duy nhất thì định thức của ma trận V ở trên phải khác 0 . Theo Định lý 1 ta có điều phải
chứng minh.
Hệ quả: khi V khả nghịch, hệ số a0 , a1 , … an−1 được xác định thông qua tích của ma trận nghịch
đảo V −1
của V và p(z0 ), p(z1 ), … p(zn−1 ).
1. Dùng n hệ số ai
2. Dùng n cặp giá trị zi , p(zi ).
Đây chính là nền tảng của việc tính nhanh tích của 2 đa thức sử dụng FFT:
1. Chọn 1 dãy zi gồm N phần tử. zi có thể chọn tuỳ ý miễn sao giá trị của chúng là đôi một khác
nhau để các đa thức p(x), q(x) và c(x) là xác định và duy nhất.
2. Chuyển 2 đa thức p(x) và q(x) sang cách biểu diễn 2. (dùng FFT)
3. Tính tích của 2 đa thức trong cách biểu diễn 2 trong O(N ). Điều này cực kỳ đơn giản, vì khi ta đã
cố định dãy zi , ta có thể tính tất cả c(zi ) = p(zi )q(zi ) trong O(N ).
4. Chuyển đa thức c(x) về cách biểu diễn 1 (dùng FFT).
n
z = 1 z ∈ C (2)
với n như đã quy ước và cũng là số nghiệm của phương trình (2) mà ta cần. Công thức Euler xác định
nghiệm thứ k của phương trình (2) là
trong đó wn là nghiệm mũ 1:
2πi 2π 2π
wn = e n
= cos + isin
n n
Dễ thấy là nghiệm nguyên thủy thứ k có thể được tính trong O(1) với n đã biết.
Một số tính chất đặc biệt của ma trận Vandermonde nghiệm
nguyên thủy
−1
Tính chất 1: Ma trận nghịch đảo V được tính theo công thức: V với
V [i,j]
−1 −1
[i, j] =
n
−1
B[i, j] = V [i, j] ∀i, j = 0, 1, . . . n − 1
Xét phép nhân hàng i của ma trận V và cột k của ma trận B, ta có:
n−1 ij −jk n−1 j(i−k)
P [i, k] = ∑ wn wn = ∑ wn
j=0 j=0
Nếu i .
n−1 0
= k : P [i, k] = P [i, i] = ∑ wn = n ∀i = 0, 1, … n − 1
j=0
n
i−k
j
Nếu i
n−1 i−k 1−w n
≠ k : P [i, k] = ∑ wn = = 0
j=0 i−k
1−w n
Tính chất 2: Chia ma trận V thành 4 phần bằng nhau kích cỡ n/2 ∗ n/2 theo 2 tiêu chí: độ lớn của
hàng so với n/2 và tính chẵn lẻ của các cột.
Phần I gồm các phần tử có chỉ số hàng 0, 1, … n/2 − 1 và chỉ số cột là chẵn 0, 2, 4, … n − 2 .
Phần I I gồm các phần tử có chỉ số hàng 0, 1, … n/2 − 1 và chỉ số cột là lẻ 1, 3, 5, … n − 1 .
Phần I I I gồm các phần tử có chỉ số hàng n/2, n/2 + 1, … n − 1 và chỉ số cột là chẵn
0, 2, 4, … n − 2 .
Phần I V gồm các phần tử có chỉ số hàng n/2, n/2 + 1, … n − 1 và chỉ số cột là lẻ
1, 3, 5, … n − 1 .
(Image Courtesy of Aalto
University)
Nói cách khác, ta tạo một ma trận mới K bằng cách chuyển tất cả các cột có chỉ số chẵn của ma trận
V lên trước, các cột có chỉ số lẻ về sau, giữ nguyên thứ tự tương đối của các cột cùng chỉ số chẵn hoặc
cùng chỉ số lẻ. Ở ma trận K này cột n − 2 của V nằm ngay trước cột 1 của V . Bốn phần
I, II, III, IV được tạo bởi cắt đều ma trận K thành 4 phần bằng nhau.
Ký hiệu KI , KI I , KI I I , KI V là bốn ma trận con của K . Tất cả các phần tử trong phần I I , I I I , I V
đều có thể tính được từ phần I theo công thức sau:
i
K I I [i, j] = wn K I [i, j] ∀i, j = 0, 1, . . . n/2 − 1
i
K I V [i, j] = −wn K I [i, j] ∀i, j = 0, 1, . . . n/2 − 1
Chứng minh: các bạn tự chứng minh hoặc xem slide số 23 trong tài liệu của trường DH Aalto ở phần
tài liệu tham khảo.
Hệ quả: Phép biến đổi Fourier ngược (inverse Fourier transform) có cùng độ phức tạp với phép biển
đổi Fourier.
Chứng minh: Sử dụng lại ký hiệu trong hình vẽ ở phần trên, ta gọi X là vector cần biến đổi Fourier và
Y là vector kết quả tương ứng. Thay vì sử dụng ma trận V để nhân với X, ta sử dụng ma trận K là kết
quả của phép biến đổi như trong Định lý 2 để nhân với X. Lưu ý là vì V đã đổi thứ tự cột nên X cũng
phải đổi thứ tự hàng: tất cả các hàng có chỉ số chẵn của X được chuyển lên trên và các hàng chỉ số lẻ
chuyển xuống dưới. Hình minh họa với n = 4 và 4 nghiệm để thay vào ma trận Vandermonde là
1, i, −1, −i :
Tách vector kết quả Y thành hai phần theo n/2 , ta được:
Ta quan sát là công thức tính nửa trên và nửa dưới của vector cột kết quả Y sử dụng chung hai hạng
tử và chỉ khác nhau về dấu của hạng tử thứ hai. Nói cách khác, chỉ cần tính được hai hạng tử tạo thành
kết quả của vector kích cỡ n/2 là ta thu được kết quả của cả vector kích cỡ n trong O(n). Theo định lý
tổng quát, độ phức tạp của cả quá trình là O(nlog2 n)
Công thức truy hồi: Từ tính chất đặc biệt của ma trận K , ta có công thức truy hồi để biến đổi một
vector cột X thành vector cột Y như sau:
i
F F T (xi=0,1,2,...n/2−1 ) = F F T (xi=0,2,4,...x ) + wn F F T (xi=1,3,5...n−1 )
n−2
i
F F T (xi=n/2,n/2+1,n/2+2,...n−1 ) = F F T (xi=0,2,4,...x ) − wn F F T (xi=1,3,5...n−1 )
n−2
Đến đây ta đã có thể hoàn thiện chương trình nhân 2 đa thức p(x), q(x) và lưu kết quả thành h(x) :
fp[] = FFT(p(x), n) // biến đổi Fourier cho p(x) và lưu các giá trị vào mảng fp
fq[] = FFT(q(x), n) // biến đổi Fourier cho q(x) và lưu các giá trị vào mảng fq
h(x) = FFT_ngược(fh) // biến đổi Fourier ngược và lưu vào kết quả
end function
Khai báo
Để sử dụng số phức trong C++ ta cần khai báo thư viện complex :
#include <complex>
Vì C++ cài đặt complex là một lớp ( class ) gồm 2 trường thực ( real() ) và ảo ( imag() ) nên khi sử
dụng ta cần chỉ định kiểu dữ liệu cho hai trường này. Hai kiểu dữ liệu thông dụng là double hoặc
long double :
Một số phiên bản cài đặt tự định nghĩa lớp số ảo bằng một struct hoặc class . Nếu lớp tự viết này
không có chức năng đặc biệt nào thì việc này là không cần thiết vì bản thân <complex> đã là một lớp
rồi. Bạn có thể xem qua file thư viện trong thư mục cài đặt trình biên dịch, ví dụ với CodeBlocks thì
đường dẫn có dạng CodeBlocks\MinGW\lib\gcc\mingw32\4.7.1\include\c++\complex (file ko có phần mở
rộng).
Trong các phần trên ta đã giả sử rằng n là lũy thừa của 2 . Để đảm bảo tính đối xứng và thuận tiện khi
cài đặt, nếu đề bài không cho trước n bậc của đa thức là lũy thừa của 2 thì ta cần chuẩn hóa thành số
lũy thừa nhỏ nhất mà lớn hơn n. Chẳng hạn với n = 10
5
thì giá trị chuẩn hóa là 217 = 131072 vì
2
16
= 65536 < 10
5
. Các hệ số của bậc cao hơn giá trị n ban đầu gán bằng 0 .
Đệ quy:
Xét một đoạn mã C++ cài đặt hàm FFT sử dụng đệ quy như sau:
void fft_slow(int n, vb& a) // biến đổi fft của vector a, lưu kết quả vào chính nó
{
if(n == 1)
{
return;
}
int i, j, k;
Có nhiều nguyên nhân làm cho FFT đệ quy chạy chậm, như trong Bước 1 thì khai báo hai vector kích cỡ
n/2 lớn như vậy và lại khai báo liên tục ở các mức đệ quy. Bản thân chương trình đệ quy cũng chạy
chậm vì chương trình phải lưu rất nhiều con trỏ stack và liên tục giải phóng bộ nhớ của biến cục bộ ở
mỗi mức đệ quy. Nhìn chung đệ quy chỉ có ý nghĩa như trong thuật toán Quy Hoạch Động khi ta
muốn tìm kết quả của một công thức truy hồi mà chỉ duyệt qua những trạng thái liên quan trực tiếp
tới kết quả. Trong FFT ta luôn phải thăm hết các ma trận con nên cài đặt FFT bằng đệ quy không có lợi
về tốc độ thực hiện.
Để cho đầy đủ thì ta cũng có hàm biến đổi FFT ngược như sau:
Khử đệ quy:
Để khử đệ quy thì ta cần phân tích mối liên hệ giữa các lời gọi đệ quy và xem các phần tử được tính
theo thứ tự nào. Hình vẽ sau đây minh họa trường hợp n = 8 :
Màu đỏ là các nhóm chẵn và màu xanh là các nhóm lẻ. Các bạn hãy dựa vào tính chẵn lẻ và để ý các số
nhị phân 0, 1 trong hình vẽ để tự viết chương trình FFT khử đệ quy hoặc giải thích tính đúng đắn của
đoạn mã sau (đây là hàm FFT đã được dùng để giải bài POST2)
#define PI acos(-1)
const int NBIT = 18;
const int N = 1<<18; // chuẩn hóa bậc của đa thức là 18
base W[N]; // mảng lưu các nghiệm nguyên thủy
// Hàm reverse bit: Đảo ngược nbit đầu tiên trong mã nhị phân của số mask
int revBit(int nbit, int mask)
{
int i, j;
for(i = 0, j = nbit - 1; i <= j; ++i, --j)
{
if( (mask >> i & 1) != (mask >> j & 1) )
{
mask ^= 1<<i;
mask ^= 1<<j;
}
}
return mask;
}
// Đi từ dưới lên trên của cây đệ quy: Hàng cuối cùng giá trị bằng với mảng được cho ban đ
// theo số có biểu diễn nhị phân ngược với chỉ số
for(i = 0; i < n; ++i)
{
j = revBit(NBIT, i);
if(i < j) swap(a[i], a[j]);
}
vb anext(n); // hàng tiếp theo
// Cứ một nhóm chẵn và một nhóm lẻ cạnh nhau thì tạo thành kết quả cho hàng ở trên
// Duyệt qua tất cả các nhóm chẵn và nhóm lẻ cạnh nó
// even = chẵn, odd = lẻ
int start_even = 0;
int start_odd = start_even + step;
while(start_even < n)
{
for(i = 0; i < step; ++i)
{
anext[start_even + i] = a[start_even + i] + W[i] * a[start_odd + i];
anext[start_even + i + step] = a[start_even + i] - W[i] * a[start_odd + i];
}
start_even += 2*step;
start_odd = start_even + step;
}
for(i = 0; i < n; ++i)
a[i] = anext[i];
}
Sau đây là cài đặt cho cả FFT xuôi và ngược. Biến invert = true cho FFT ngược.
int i, j, k;
for(i = 0; i < n; ++i)
{
j = revBit(NBIT, i);
if(i < j) swap(a[i], a[j]);
}
vb anext(n);
int start_even = 0;
int start_odd = start_even + step;
while(start_even < n)
{
for(i = 0; i < step; ++i)
{
anext[start_even + i] = a[start_even + i] + W[i] * a[start_odd + i];
anext[start_even + i + step] = a[start_even + i] - W[i] * a[start_odd + i];
}
start_even += 2*step;
start_odd = start_even + step;
}
for(i = 0; i < n; ++i)
a[i] = anext[i];
}
if (invert) {
for(i = 0; i < n; ++i)
a[i] /= n;
}
}
Một số cách cài đặt khác sử dụng con trỏ cũng làm tăng tốc độ thực thi, có thể xem thêm trong trang
của emaxx phần tài liệu tham khảo. Cũng trong trang của emaxx có thể tìm thấy cách cài đặt gộp hai
hàm fft và inverse_fft lại làm một sử dụng một biến bool invert làm cho code ngắn gọn hơn.
Bài tập luyện tập
VNOJ POST2
FFT problems on Codeforces
FFT problems by a2oj.com
SumOfArrays - Topcoder SRM 603 và Hướng dẫn giải
Add a comment...