Back to Home

Fast Fourier Transform - Nhân đa thức nhanh

Bài toán nhân đa thức

Nhân hai đa thức là một thao tác cơ bản trong lập trình, nhất là trong mật mã. Giả sử chúng ta có hai đa thức bậc n1n-1:

  • A(x)=a0+a1x+a2x2++an1xn1A(x) = a_0 + a_1x + a_2x^2 + \dots + a_{n-1}x^{n-1}

  • B(x)=b0+b1x+b2x2++bn1xn1B(x) = b_0 + b_1x + b_2x^2 + \dots + b_{n-1}x^{n-1}

Cách tiếp cận tự nhiên nhất mà chúng ta được học ở trường phổ thông là phép nhân phân phối (Elementary Multiplication). Để tìm hệ số của đa thức tích C(x)=A(x)B(x)C(x) = A(x) \cdot B(x), ta lấy từng hạng tử của AA nhân với mọi hạng tử của BB. Nếu mỗi đa thức có NN hệ số, chúng ta cần thực hiện N×N=N2N \times N = N^2 phép nhân số thực/số phức. Trong khoa học máy tính, đây là độ phức tạp O(N2)O(N^2). Với N1.000.000N \ge 1.000.000 (trong các bài toán xử lý tín hiệu hoặc mã hóa), số phép toán lên tới 1.000 tỷ, khiến các hệ thống hiện đại cũng phải "đầu hàng" về mặt thời gian thực.

Sự xuất hiện của FFT (Fast Fourier Transform), cụ thể là thuật toán Cooley-Tukey, đã thay đổi hoàn toàn cuộc chơi này bằng cách đưa độ phức tạp về mức O(NlogN)O(N \log N), biến những bài toán không thể thành có thể.

Hệ số (Coefficient) vs. Điểm (Point-Value)

Thông thường, chúng ta định nghĩa đa thức qua các hệ số của nó. Nhưng thực ra, chúng ta vẫn có thể định nghĩa một đa thức thông qua các điểm phân biệt mà đồ thị của nó đi qua. Theo định lý cơ bản về đa thức: Một đa thức bậc n1n-1 được xác định duy nhất bởi tập hợp nn cặp điểm (xi,yi)(x_i, y_i) phân biệt.

Ví dụ:

  • Qua 2 điểm, ta xác định được duy nhất 1 đường thẳng (bậc 1).

  • Qua 3 điểm, ta xác định được duy nhất 1 đường parabol (bậc 2).

Hãy tưởng tượng bạn có hai đa thức AABB đã được chuyển sang dạng điểm tại cùng một tập hợp các giá trị {x0,x1,,x2n1}\{x_0, x_1, \dots, x_{2n-1}\}.

Để tìm đa thức tích C=ABC = A \cdot B ở dạng điểm, chúng ta chỉ cần tính:

C(xi)=A(xi)B(xi)C(x_i) = A(x_i) \cdot B(x_i)

Phép toán này chỉ tốn đúng 2n2n phép nhân, tức là độ phức tạp O(N)O(N), yeah!!!

Vậy tại sao nhanh như vậy mà ta không dùng nó ngay từ đầu? Đó là vì chi phí chuyển đổi:

  1. Chuyển từ Hệ số \rightarrow Điểm (Evaluation): Nếu tính toán thông thường (thế xx vào đa thức), ta mất O(N2)O(N^2).

  2. Chuyển từ Điểm \rightarrow Hệ số (Interpolation): Dùng các phương pháp như Newton hay Lagrange cũng mất O(N2)O(N^2).

FFT cho phép chúng ta thực hiện cả hai quá trình chuyển đổi trên chỉ với độ phức tạp O(NlogN)O(N \log N).

Thuật toán Cooley-Tukey

Để chuyển một đa thức từ dạng Hệ số sang dạng Điểm trong O(NlogN)O(N \log N), thuật toán Cooley-Tukey sử dụng chiến lược Chia để trị (Divide and Conquer) dựa trên một lựa chọn toán học cực kỳ thông minh: Căn đơn vị phức (Complex Roots of Unity).

Căn đơn vị phức

Nếu ta chọn các điểm xx ngẫu nhiên, ta vẫn mất O(N2)O(N^2) để tính giá trị đa thức. Tuy nhiên, nếu ta chọn xx là các nghiệm của phương trình xN=1x^N = 1, chúng ta có những tính chất đối xứng tuyệt vời. Các điểm này nằm trên vòng tròn đơn vị trong mặt phẳng phức, được xác định bởi công thức:

ωNk=ej2πkN=cos(2πkN)+jsin(2πkN)\omega_N^k = e^{j\frac{2\pi k}{N}} = \cos\left(\frac{2\pi k}{N}\right) + j\sin\left(\frac{2\pi k}{N}\right)

Với k=0,1,,N1k = 0, 1, \dots, N-1.

Tính chất: (ωNk)2=ωN/2k(\omega_N^k)^2 = \omega_{N/2}^k. Điều này cho phép ta thu nhỏ bài toán kích thước NN thành bài toán kích thước N/2N/2 một cách đồng nhất.

Chiến lược Chia để trị: Tách Chẵn và Lẻ

Giả sử ta có đa thức A(x)A(x) bậc N1N-1(với NN là lũy thừa của 2). Ta tách các hệ số của A(x)A(x) thành hai đa thức con bậc N/21N/2 - 1:

  • Aeven(y)=a0+a2y+a4y2++aN2yN/21A_{even}(y) = a_0 + a_2y + a_4y^2 + \dots + a_{N-2}y^{N/2-1} (Chứa các hệ số ở vị trí chẵn)

  • Aodd(y)=a1+a3y+a5y2++aN1yN/21A_{odd}(y) = a_1 + a_3y + a_5y^2 + \dots + a_{N-1}y^{N/2-1} (Chứa các hệ số ở vị trí lẻ)

Khi đó, đa thức ban đầu có thể biểu diễn qua hai đa thức con như sau:

A(x)=Aeven(x2)+xAodd(x2)A(x) = A_{even}(x^2) + x \cdot A_{odd}(x^2)

Cấu trúc cánh bướm

Nhờ tính chất chu kỳ của số phức (ωNk+N/2=ωNk\omega_N^{k + N/2} = -\omega_N^k), khi ta tính giá trị của A(x)A(x) tại các điểm ωNk\omega_N^k, chúng ta nhận thấy một sự lặp lại thú vị:

  1. Với nửa đầu (k<N/2k < N/2):

    A(ωNk)=Aeven(ωN/2k)+ωNkAodd(ωN/2k)A(\omega_N^k) = A_{even}(\omega_{N/2}^k) + \omega_N^k A_{odd}(\omega_{N/2}^k)
  2. Với nửa sau (kN/2k \geq N/2, đặt k=m+N/2k = m + N/2):

    A(ωNm+N/2)=Aeven(ωN/2m)ωNmAodd(ωN/2m)A(\omega_N^{m+N/2}) = A_{even}(\omega_{N/2}^m) - \omega_N^m A_{odd}(\omega_{N/2}^m)

Kết quả: Mỗi bước tính toán ở tầng dưới cung cấp dữ liệu cho hai giá trị ở tầng trên chỉ bằng một phép nhân và hai phép cộng/trừ. Đây chính là cấu trúc Butterfly nổi tiếng giúp giảm số lượng phép tính từ N2N^2 xuống còn Nlog2NN \log_2 N.

Bức ảnh này giải thích chính xác cơ chế "phép màu" đã được đề cập:

  • Đầu vào (bên trái): Hai giá trị A[k]A[k]A[k+N/2]A[k+N/2] từ tầng tính toán trước đó.

  • Hệ số xoay (Twiddle Factor - WNkW_N^k): Giá trị phức được nhân với đầu vào thứ hai trước khi kết hợp.

  • Điểm kết hợp (Cánh bướm):

    • Phép cộng (trên): Tạo ra đầu ra thứ nhất: A[k]next=A[k]+WNkA[k+N/2]A[k]_{next} = A[k] + W_N^k A[k+N/2].

    • Phép trừ (dưới): Tạo ra đầu ra thứ hai: A[k+N/2]next=A[k]WNkA[k+N/2]A[k+N/2]_{next} = A[k] - W_N^k A[k+N/2].

Cấu trúc đối xứng này cho phép máy tính tái sử dụng các kết quả tính toán trung gian, biến đổi độ phức tạp từ O(N2)O(N^2) thành O(NlogN)O(N \log N).

Quy trình nhân đa thức

Để nhân hai đa thức AABB, quy trình gồm 3 bước:

Bước 1: Mở rộng và Biến đổi (Padding & Forward FFT)

Trước khi bắt đầu, có một chi tiết kỹ thuật cực kỳ quan trọng: Xử lý bậc của đa thức kết quả.

Nếu AA có bậc n1n-1BB có bậc m1m-1, thì đa thức tích CC sẽ có bậc (n1)+(m1)(n-1) + (m-1).

  • Zero Padding: Chúng ta phải thêm các hệ số 00 vào AABB sao cho tổng số hệ số của mỗi đa thức ít nhất bằng bậc của C+1C+1, và phải là một lũy thừa của 2 (ví dụ: 2k2^k).

  • Thực thi: Áp dụng FFT Cooley-Tukey để chuyển AABB từ dạng hệ số sang dạng điểm (tại các căn đơn vị ωNk\omega_N^k).

    • AFFT{A(ωN0),A(ωN1),,A(ωNN1)}A \xrightarrow{FFT} \{A(\omega_N^0), A(\omega_N^1), \dots, A(\omega_N^{N-1})\}

    • BFFT{B(ωN0),B(ωN1),,B(ωNN1)}B \xrightarrow{FFT} \{B(\omega_N^0), B(\omega_N^1), \dots, B(\omega_N^{N-1})\}

Bước 2: Nhân trong miền giá trị (Point-wise Multiplication)

Với các giá trị điểm đã có, ta tính giá trị của đa thức tích CC tại từng điểm tương ứng:

C(ωNk)=A(ωNk)×B(ωNk)C(\omega_N^k) = A(\omega_N^k) \times B(\omega_N^k)

Chỉ cần đúng NN phép nhân số phức, chúng ta đã có đầy đủ đại diện dạng điểm của đa thức tích.

Bước 3: Biến đổi ngược (Inverse FFT - IFFT)

Bây giờ, chúng ta đang có các giá trị C(ωNk)C(\omega_N^k), nhưng mục tiêu là phải xác định kết quả là các hệ số {c0,c1,,cN1}\{c_0, c_1, \dots, c_{N-1}\}. Lúc này, ta cần đến quá trình biến đổi ngược Inverse FFT. Thú vị thay, thuật toán cho IFFT gần như y hệt FFT. Sự khác biệt duy nhất nằm ở hai điểm:

  • Thay thế hệ số xoay ωNk\omega_N^k bằng nghịch đảo của nó (tương đương với việc đổi dấu phần ảo trong số phức).

  • Sau khi tính xong, tất cả kết quả phải được chia cho NN

cn=1Nk=0N1C(ωNk)ej2πNnkc_n = \frac{1}{N} \sum_{k=0}^{N-1} C(\omega_N^k) e^{j\frac{2\pi}{N}nk}

Tổng kết độ phức tạp

Hãy nhìn lại toàn bộ hành trình:

  1. FFT cho A và B: 2×O(NlogN)2 \times O(N \log N)

  2. Nhân điểm: O(N)O(N)

  3. IFFT cho C: O(NlogN)O(N \log N)

Tổng cộng: O(NlogN)\mathbf{O(N \log N)}.

Phân tích Pseudo Code

Để cài đặt FFT Cooley-Tukey, chúng ta sử dụng cấu trúc đệ quy chia mảng hệ số thành hai nửa: vị trí chẵn và vị trí lẻ.

  1. Hàm FFT (Hệ số \rightarrow Điểm)

HÀM FFT(Mảng_Hệ_Số A):
    N = Độ dài của A
    NẾU N == 1: 
        TRẢ VỀ A  // Trường hợp cơ bản: Đa thức bậc 0 là chính nó

    // BƯỚC 1: CHIA (Divide)
    A_chẵn = [A[0], A[2], ..., A[N-2]]
    A_lẻ   = [A[1], A[3], ..., A[N-1]]

    // BƯỚC 2: TRỊ (Conquer - Đệ quy)
    Y_chẵn = FFT(A_chẵn)
    Y_lẻ   = FFT(A_lẻ)

    // BƯỚC 3: KẾT HỢP (Combine - Cấu trúc Cánh bướm)
    Mảng_Kết_Quả Y = Mảng rỗng kích thước N
    Hệ_số_xoay_gốc = e^(i * 2π / N)
    w = 1

    CHO k CHẠY TỪ 0 ĐẾN N/2 - 1:
        // Tính toán dựa trên tính đối xứng và chu kỳ
        Y[k]       = Y_chẵn[k] + w * Y_lẻ[k]
        Y[k + N/2] = Y_chẵn[k] - w * Y_lẻ[k]
        
        w = w * Hệ_số_xoay_gốc // Xoay dần trên vòng tròn đơn vị

    TRẢ VỀ Y
  1. Quy trình nhân hai đa thức P1P_1P2P_2

HÀM NHÂN_ĐA_THỨC(P1, P2):
    1. Padding: Thêm các số 0 vào P1, P2 sao cho độ dài N là lũy thừa của 2 
       (N phải đủ lớn để chứa bậc của đa thức kết quả).

    2. Chuyển đổi sang dạng điểm:
       Điểm_P1 = FFT(P1)
       Điểm_P2 = FFT(P2)

    3. Nhân trực tiếp (Point-wise):
       Điểm_Kết_Quả = Mảng chứa (Điểm_P1[i] * Điểm_P2[i]) với mọi i từ 0 đến N-1

    4. Chuyển ngược về dạng hệ số:
       Hệ_Số_Kết_Quả = IFFT(Điểm_Kết_Quả) 
       (IFFT dùng thuật toán y hệt FFT nhưng đổi dấu số phức và chia kết quả cho N)

    TRẢ VỀ Hệ_Số_Kết_Quả

Demo

Comments

0/300

Leave name/email blank to comment anonymously

No comments yet. Be the first to comment!