Biến đổi Fourier rời rạc

Phần này mình dịch từ [11].

Giới thiệu

Cho số \(t \in \mathbb{N}\)\(n = 2^t\).

Gọi \(R\) là vành với đơn vị \(1\). Vành \(R\) chứa phần tử \(2^{-1}\) là nghịch đảo của phần tử \(2\). Hơn nữa vành \(R\) cũng chứa phần tử \(\zeta_{2n}\) là một nghiệm cố định nào đó của phương trình \(x^n + 1 = 0\). Đặt \(\zeta_n = \zeta_{2n}^2\).

Ví dụ 13

Trên \(\mathbb{C}\) thì \(\zeta_{2n} = e^{\pi i/n}\) là một nghiệm của phương trình \(x^n + 1 = 0\). Khi đó \(\zeta_n = \zeta_{2n}^2 = e^{2\pi i /n}\).

Bổ đề 1

Phần tử \(\zeta_{2n}\) sinh ra một nhóm vòng (cyclic group) với order \(2n\) trong nhóm nhân của vành \(R\).

Giả sử \((f_0, \ldots, f_{n-1}) \in R^n\) là vector bất kì. Khi đó:

Biến đổi Fourier loại 1 của vector trên là vector \(n\) chiều \((\hat f_0, \ldots, \hat f_{n-1})\) thuộc \(R^n\) xác định bởi:

\[\hat f_i = \sum_{j=0}^{n-1} \zeta_n^{ij} f_j, \quad i = 0, 1, \ldots, n-1.\]

Biến đổi Fourier loại 2 của vector trên là vector \(n\) chiều \((\check f_1, \check f_3, \ldots, \check f_{2n-1})\) thuộc \(R^n\) xác định bởi:

\[\check f_i = \sum_{j=0}^{n-1} \zeta_{2n}^{ij} f_j, \quad i = 1, 3, \ldots, 2n-1.\]

Ví dụ 14

Mình lấy ví dụ với vector \((f_0, f_1, f_2) = (1, 2, 3)\) thuộc \(\mathbb{C}^3\).

Chọn nghiệm \(\zeta_{2n} = e^{\pi i / 3}\) của phương trình \(x^3 + 1 = 0\). Khi đó \(\zeta_n = \zeta_{2n}^2 = e^{2\pi i / 3}\).

Biến đổi Fourier loại 1 lúc này là:

\[\begin{split}\hat{f}_0 & = \left(e^{2\pi i/3}\right)^{0 \cdot 0} \cdot 1 + \left(e^{2\pi i/3}\right)^{0 \cdot 1} \cdot 2 + \left(e^{2\pi i/3}\right)^{0 \cdot 2} \cdot 3 \\ & = 1 + 2 + 3 = 6. \\ \hat{f}_1 & = \left(e^{2\pi i/3}\right)^{1 \cdot 0} \cdot 1 + \left(e^{2\pi i/3}\right)^{1 \cdot 1} \cdot 2 + \left(e^{2\pi i/3}\right)^{1 \cdot 2} \cdot 3 \\ & = 1 + 2 \cdot e^{2\pi i/3} + 3 \cdot 3 \cdot e^{4\pi i/3} \\ \hat{f}_2 & = \left(e^{2\pi i/3}\right)^{2 \cdot 0} \cdot 1 + \left(e^{2\pi i/3}\right)^{2 \cdot 1} \cdot 2 + \left(e^{2\pi i/3}\right)^{2 \cdot 2} \cdot 3 \\ & = 1 + 2 \cdot e^{4\pi i/3} + 3 \cdot e^{2\pi i/3}.\end{split}\]

Chú ý rằng \(e^{2\pi i} = 1\) nên biểu thức cuối của \(\hat{f}_2\) rút gọn còn \(e^{2\pi i / 3}\) (\(8 \equiv 2 \bmod 3\)).

Tiếp theo ta biểu diễn \(e^{4\pi i / 3}\) theo \(e^{2\pi i / 3}\) như sau. Ta xét

\[\begin{split}e^{2zi} & = \cos 2z + i\sin 2z \\ & = 2\cos^2 z - 1 + 2i \sin z \cos z \\ & = 2\cos z (\cos z + i\sin z) - 1 \\ & = 2\cos z \cdot e^{zi} - 1.\end{split}\]

Thay \(z = 2\pi / 3\) vào kết quả trên ta được

\[e^{4\pi i / 3} = 2\cos \frac{2\pi}{3} \cdot e^{2\pi i/3} - 1 = -e^{2\pi i / 3} - 1.\]

Như vậy kết quả của biến đổi Fourier rời rạc bên trên là

\[(\hat{f}_0, \hat{f}_1, \hat{f}_2) = (6, -e^{2\pi i / 3} - 2, e^{2\pi i / 3} - 1).\]

Ở ví dụ trên chúng ta tính biến đổi Fourier cho vector có độ dài không phải lũy thừa của \(2\). Sau đây chúng ta sẽ xem xét một ví dụ với vector có độ dài là lũy thừa của \(2\) và ở phần sau sẽ tối ưu tính toán với biến đổi Fourier nhanh.

Ví dụ 15

Mình lấy ví dụ với vector \((f_0, f_1, f_2, f_3) = (1, 2, 3, 4)\) thuộc \(\mathbb{C}^4\).

Chọn nghiệm \(\zeta_{2n} = e^{i\pi / 4}\) của phương trình \(x^4 + 1 = 0\). Khi đó \(\zeta_n = \zeta_{2n}^2 = e^{i \pi / 2} = i\).

Biến đổi Fourier loại 1 lúc này là:

\[\begin{split}\hat{f}_0 & = 1 \cdot 1 + 2 \cdot 1 + 3 \cdot 1 + 4 \cdot 1 = 10, \\ \hat{f}_1 & = 1 \cdot 1 + 2 \cdot i + 3 \cdot (-1) + 4 \cdot (-i) = -2 - 2i, \\ \hat{f}_2 & = 1 \cdot 1 + 2 \cdot (-1) + 3 \cdot 1 + 4 \cdot (-1) = -2, \\ \hat{f}_3 & = 1 \cdot 1 + 2 \cdot (-i) + 3 \cdot (-1) + 4 \cdot i = -2 + 2i.\end{split}\]

Đối với vector độ dài \(n = 2^2\) thì \(t = 2\), chúng ta cần \(n - 1\) phép cộng và \(n\) phép nhân với lũy thừa của \(\zeta_{n}\) để tính mỗi \(\hat{f}_i\), với \(i = 0, 1, 2, 3\). Tổng quát, ta cần tất cả \((n + n - 1) \cdot n\) phép tính cộng và nhân với lũy thừa của \(\zeta_n\). Độ phức tạp để tính biến đổi Fourier rời rạc là \(O(n^2)\).

Nhận xét 8

Các phần tử \(\hat f_i\) là giá trị của đa thức \(F(x) = \sum\limits_{j=0}^{n-1} f_j x^j \in R[x]\) tại điểm \(x = \zeta_n^i\), các phần tử \(\check f_i\) là giá trị của đa thức \(F(x)\) tại \(x = \zeta_{2n}^i\) khi \(i\) lẻ.

Bổ đề 2

Ta có biểu diễn ngược như sau:

(1)\[f_i = n^{-1} \sum_{j=0}^{n-1} \hat f_j \zeta_n^{-ij},\]
(2)\[\begin{split}f_i = n^{-1} \sum_{\substack{1 \leqslant j \leqslant 2n-1 \\ j \ \text{lẻ}}} \check f_j \zeta_{2n}^{-ij},\end{split}\]

với \(n^{-1} = (2^{-1})^t \in R\).

Việc tính toán biến đổi Fourier rời rạc bằng (1)(2) cần \(O(n^2)\) phép tính cộng và nhân trong vành \(R\). Phần sau sẽ trình bày phương pháp tính biến đổi Fourier rời rạc với độ phức tạp \(O(n \log n)\). Phương pháp này được gọi là biến đổi Fourier nhanh (hay Fast Fourier Transform, FFT, быстрое преобразование Фурье).

Biến đổi Fourier nhanh

Định lí 6

Biến đổi Fourier rời rạc \((\hat f_0, \ldots, \hat f_{n-1})\) có thể được tính bằng:

  • \(nt\) phép cộng trong vành \(R\);

  • \(nt\) phép nhân với lũy thừa của \(\zeta_n\) trong vành \(R\).

Biến đổi Fourier rời rạc \((\check f_1, \ldots, \check f_{2n-1})\) có thể được tính bằng:

  • \(nt\) phép cộng trong vành \(R\);

  • \(nt\) phép nhân với lũy thừa của \(\zeta_{2n}\) trong vành \(R\).

Nếu có \((\hat f_i)\)\((\check f_i)\) thì có thể tính được vector \((f_0, \ldots, f_{n-1})\) bằng:

  • \(nt\) phép cộng trong vành \(R\);

  • \(nt\) phép nhân với lũy thừa của \(\zeta_{2n}\) trong vành \(R\);

  • \(n\) phép nhân với \(n^{-1} \in R\).

Ví dụ 16

Xét vector \((f_0, f_1, f_2, f_3) = (1, 2, 3, 4)\) thuộc \(\mathbb{C}^4\) như bên trên.

Ở đây, \(n = 4 = 2^2\)\(t = 2\).

Chọn nghiệm \(\zeta_{8} = e^{i\pi / 4}\) của phương trình \(x^4 + 1 = 0\). Khi đó \(\zeta_4 = \zeta_{8}^2 = e^{i \pi / 2} = i\).

Ta chia vector thành hai phần:

  • vector con gồm các phần tử ở vị trí chẵn là \((f_0, f_2) = (1, 3)\);

  • vector con gồm các phần tử ở vị trí lẻ là \((f_1, f_3) = (2, 4)\).

Đặt \(\zeta_2 = \zeta_{4}^2 = -1\).

Khi đó, với vector con \((f_0, f_2) = (1, 3)\) ta tính:

\[\begin{split}F_0 & = f_0 + \zeta_{2}^0 \cdot f_2 = 1 + 1 \cdot 3 = 4, \\ F_1 & = f_0 + \zeta_{2}^1 \cdot f_2 = 1 + (-1) \cdot 3 = -2.\end{split}\]

Tương tự, với vector con \((f_1, f_3) = (2, 4)\) ta tính

\[\begin{split}F_2 & = f_1 + \zeta_{2}^0 \cdot f_3 = 2 + 1 \cdot 4 = 6, \\ F_3 & = f_1 + \zeta_{2}^1 \cdot f_3 = 2 + (-1) \cdot 4 = -2.\end{split}\]

Kết hợp hai vector \((F_0, F_2)\)\((F_3, F_4)\) ta tính được biến đổi Fourier nhanh.

\[\begin{split}\hat{f}_0 & = F_0 + \zeta_4^0 \cdot F_2 = 4 + 1 \cdot 6 = 10, \\ \hat{f}_2 & = F_0 + \zeta_4^2 \cdot F_2 = 4 + (-1) \cdot 6 = -2, \\ \hat{f}_1 & = F_1 + \zeta_4^1 \cdot F_3 = -2 + i \cdot (-2) = -2 - 2 i, \\ \hat{f}_3 & = F_1 + \zeta_4^3 \cdot F_3 = -2 + (-i) \cdot (-2) = -2 + 2i.\end{split}\]

Ở đây, hệ số \(\hat{f}_i\) ở vị trí lẻ sẽ được tính nhờ các \(F_i\) ở vị trí lẻ cùng với lũy thừa lẻ của \(\zeta_n\), và ngược lại, hệ số \(\hat{f}_i\) ở vị trí chẵn sẽ được tính nhờ các \(F_i\) ở vị trí chẵn cùng với lũy thừa chẵn của \(\zeta_n\).

Như vậy kết quả là

\[(\hat{f}_0, \hat{f}_1, \hat{f}_2, \hat{f}_3) = (10, -2 - 2i, -2, -2 + 2i),\]

giống với ví dụ ở trên. Tuy nhiên chúng ta chỉ dùng \(4\) phép cộng và \(4\) phép nhân với \(\zeta_{2}\) để tính các \(F_i\), cộng thêm \(4\) phép cộng và \(4\) phép nhân với \(\zeta_4\) để tính các \(\hat{f}_i\). Như vậy tổng cộng ta dùng \(8 = n t\) phép cộng, và \(8 = n t\) phép nhân cho các lũy thừa của \(\zeta_4\) (vì \(\zeta_{2}\) cũng là lũy thừa của \(\zeta_4\)).

Biến đổi Fourier và phép nhân đa thức

Bổ đề 3

Một phép nhân trong vành \(R[x]/(x^{2^t} + 1)\) có thể được tính bởi:

  • \(n = 2^t\) phép nhân trong vành \(R\);

  • \(3nt\) phép cộng trong vành \(R\);

  • \(3nt\) phép nhân với lũy thừa của \(\zeta_{2n}\) trong vành \(R\);

  • \(n\) phép nhân với \(n^{-1}\) trong vành \(R\).

Nhận xét 9

Bổ đề không áp dụng khi \(R = \mathbb{Z}\)\(R = \mathbb{Q}\) vì hai vành này không có phần tử \(\zeta_{2n}\) (căn bậc \(n\) của đơn vị).

Sau đây ta chứng minh Bổ đề 3.

Hệ quả 1

Đặt \(T\) là vành giao hoán với đơn vị, \(2^{-1} \in T\), \(\zeta_{4n} = \zeta_{2^{t + 2}} \in T\) là nghiệm phương trình \(x^{2n} + 1 = 0\). Nếu \(F(x), G(x) \in T[x]\), \(\deg F(x) < n\), \(\deg G(x) < n\) thì tích \(F(x) \cdot G(x)\) có thể tính với:

  • \(2n\) phép nhân trong \(T\);

  • \(6n(t+1)\) phép cộng trong \(T\);

  • \(6n(t+1)\) phép nhân với lũy thừa của \(\zeta_{4n}\) trong \(T\);

  • \(2n\) phép nhân với \((2^{-1})^{t+1}\) trong \(T\).

Đặt \(T\) là vành giao hoán với đơn vị \(1\) và có phần tử \(2^{-1}\).

Ta hiểu phép cộng trong \(T\) là cả phép cộng lẫn phép trừ (cộng cho số đối). Ta cần bổ đề cơ sở sau.

Bổ đề 4

Nếu \(t \geqslant 2\) thì một phép nhân trong vành \(T[x] / (x^{2^t} + 1)\) có thể thực hiện với:

  • \(O(2^t \cdot t)\) phép nhân trong \(T\);

  • \(O(2^t \cdot t \cdot \log t)\) phép cộng trong \(T\).

Nhận xét 10

Kết quả trên có thể áp dụng với vành số nguyên \(\mathbb{Z}\) nếu ta xem \(T\) là vành như sau:

\[T = \left\{\frac{m}{2^k} : m \in \mathbb{Z}, k \in \mathbb{Z}_{\geqslant 0}\right\}, \quad T \supseteq \mathbb{Z}.\]

Trên máy tính, mỗi phần tử của \(T\) được lưu chính xác, ví dụ dưới dạng cặp \((m, k)\).

Ta sẽ xem xét định lí quan trọng tiếp theo rồi quay lại chứng minh Bổ đề 4.

Định lí 7

Phép nhân hai đa thức có bậc không quá \(n\) trong vành \(T[x]\), với \(n \geqslant 3\), được tính với \(M(n) = O(n \log n)\) phép nhân trong \(T\)\(A(n) = O(n \log n \log \log n)\) phép cộng trong \(T\).

Tiếp theo chúng ta quay lại chứng minh Bổ đề 4.

Ví dụ nhân đa thức với Sympy

Để ví dụ, mình sẽ viết chương trình Python nhân hai đa thức bậc \(4\) trên \(\mathbb{Q}\) trong modulo \(x^4 + 1\).

Thuật toán ở các chứng minh trên là thuật toán Cooley-Tukey FFT.

Đầu tiên mình sẽ viết hàm đảo ngược bit (dùng cho radix-2 decimal-in-time).

def _reverse_bit(n: int, nbits: int) -> int:
  m = 0
  for _ in range(nbits):
      m = (m << 1) + (n & 1)
      n = n >> 1
  return m

assert _reverse_bit(3, 3) == 6
assert _reverse_bit(1, 3) == 4

FFT là thuật toán chia để trị -- để tính toán FFT cho vector độ dài \(n = 2^t\), ta sẽ tính trên hai vector độ dài \(n / 2\) và kết hợp kết quả lại.

from sympy import pi, sin, cos, I, simplify
from sympy.codegen.cfunctions import log2

def _mult_W(v: list[int]) -> list[int]:
  n = len(v)
  w = cos(2 * pi / n) + I * sin(2 * pi / n)
  u = [0] * n
  for i in range(0, n // 2):
      u[i] = v[i] + w**i * v[i + n // 2]
      u[i + n // 2] = v[i] + w**(i + n // 2) * v[i + n // 2]
  return u

def _fft(v: list[int]) -> list[int]:
    n = len(v)

    if n == 2:
        return _mult_W(v)
    else:
        u = _fft(v[:n // 2]) + _fft(v[n // 2:])
        return _mult_W(u)

def fft(v: list[int]) -> list[int]:
    n = len(v)
    b = int(log2(n))
    idx = [_reverse_bit(i, b) for i in range(n)]
    v = [v[idx[i]] for i in range(n)]

    return list(map(simplify, _fft(v)))

def _mult_iW(v: list[int]) -> list[int]:
    n = len(v)
    w = cos(-2 * pi / n) + I * sin(-2 * pi / n)
    u = [0] * n
    for i in range(0, n // 2):
        u[i] = v[i] + w**i * v[i + n // 2]
        u[i + n // 2] = v[i] + w**(i + n // 2) * v[i + n // 2]
    return u

def _ifft(v: list[int]) -> list[int]:
    n = len(v)
    if n == 2:
        return _mult_iW(v)
    else:
        u = _ifft(v[:n // 2]) + _ifft(v[n // 2:])
        return _mult_iW(u)

def ifft(v: list[int]) -> list[int]:
    n = len(v)
    b = int(log2(n))
    idx = [_reverse_bit(i, b) for i in range(n)]
    v = [v[idx[i]] for i in range(n)]

    return [simplify(i / n) for i in _ifft(v)]

Tiếp theo mình kiểm tra các hàm đã viết bên trên.

assert fft([2, 3, 0, 0]) == [5, 2 + 3*I, -1, 2 - 3*I]
assert ifft([5, 2 + 3*I, -1, 2 - 3*I]) == [2, 3, 0, 0]

import random
v = [random.randint(-4, 4) for _ in range(8)]
assert ifft(fft(v)) == v, fft(v)

Ví dụ, để nhân hai đa thức

\[\begin{split}a(x) & = 1 + 2x - x^2 + 3x^3, \\ b(x) & = -1 - 4x + 3x^2 - 2x^3,\end{split}\]

trong modulo \(x^4 + 1\), đầu tiên mình viết hệ số của mỗi đa thức thành vector theo số mũ tăng dần, như vậy

\[\bm{a} = (1, 2, -1, 3), \quad \bm{b} = (-1, -4, 3, -2).\]

Tiếp theo, mình sẽ thêm vào sau các vector \(\bm{a}\)\(\bm{b}\) các số \(0\) để đạt độ dài là lũy thừa của \(2\) nhưng lớn hơn bậc của modulo. Ở đây modulo là \(x^4 + 1\) nên mình sẽ thêm các số \(0\) để đạt độ dài \(4 \cdot 2 = 8\).

\[\bm{a}' = (1, 2, -1, 3, 0, 0, 0, 0), \quad \bm{b}' = (-1, 4, 3, -2, 0, 0, 0, 0).\]

Tiếp theo, mình áp dụng FFT cho hai vector \(\bm{a}'\)\(\bm{b}'\).

a = [1, 2, -1, 3, 0, 0, 0, 0]
b = [-1, -4, 3, -2, 0, 0, 0, 0]

fa = fft(a)
fb = fft(b)

Đặt

\[\begin{split}\bm{f}_a & = \mathsf{FFT}(\bm{a}') = (a_0, a_1, \ldots, a_7), \\ \bm{f}_b & = \mathsf{FFT}(\bm{b}') = (b_0, b_1, \ldots, b_7).\end{split}\]

Khi đó ta tính vector \(\bm{f}_c\) theo là tích theo cặp của \(\bm{f}_a\)\(\bm{f}_b\), nghĩa là

\[\bm{f}_c = (c_0, c_1, \ldots, c_7), \ c_i = a_i \cdot b_i.\]

Tiếp theo, ta tính \(\bm{c}'\) là biến đổi Fourier rời rạc ngược của \(\bm{f}_c\):

\[\bm{c}' = \mathsf{FFT}^{-1}(\bm{f}_c) = (C_0, C_1, \ldots, C_7).\]

Khi đó, ta tính vector \(\bm{c}\) độ dài \(4\) theo công thức

\[\bm{c} = (C_i - C_{i+4})_{i = 0}^3.\]
fc = [i * j for i, j in zip(fa, fb)]
fc = list(map(simplify, ifft(fc)))
c = [fc[i] - fc[i + 4] for i in range(4)]
print(c)

Kết quả là vector \((18, -17, 2, 5)\).

Kiểm tra kết quả bằng tay, mình có

\[\begin{split}a(x) \cdot b(x) & = (1 + 2x - x^2 + 3x^3) \cdot (-1 - 4x + 3x^2 - 2x^3) \\ & = -1 - 4x + 3x^2 - 2x^3 \\ & - 2x - 8x^2 + 6x^3 - 4x^4 \\ & + x^2 + 4x^3 - 3x^4 + 2x^5 \\ & - 3x^3 - 12x^4 + 9x^5 - 6x^6 \\ & = -1 - 6x - 4x^2 + 5x^3 - 19x^4 + 11x^5 - 6x^6.\end{split}\]

Trong modulo \(x^4 + 1\) ta có thể thay thế \(x^4\) thành \(-1\), như vậy kết quả trên tương đương

\[\begin{split}a(x) \cdot b(x) & = -1 - 6x - 4x^2 + 5x^3 - 19x^4 + 11x^5 - 6x^6 \\ & = -1 - 6x - 4x^2 + 5x^3 - 19 \cdot (-1) + 11x \cdot (-1) - 6x^2 \cdot (-1) \\ & = 18 - 17x + 2x^2 + 5x^3 \bmod x^4 + 1.\end{split}\]

Nếu viết hệ số của kết quả dưới dạng vector theo số mũ tăng dần thì mình có \((18, -17, 2, 5)\), bằng đúng phép tính với thuật toán Cooley-Tukey.

Một lý do khiến mình viết code trong khi nhiều thư viện Python như numpy, sympy, sagemath đã hỗ trợ là vì trong [11] (tài liệu tham khảo chính của bài viết này) thì DFT thuận sử dụng \(\zeta_{2n}\) là nghiệm của \(x^n + 1\) nên nếu xét trên \(\mathbb{C}\) thì \(\zeta_{2n} = e^{i \pi / n}\). Trong khi đó các tài liệu khác như wikipedia, numpy, sympy, ... sử dụng \(e^{-i \pi / n}\) để tính DFT thuận. Ngược lại, [11] sử dụng \(e^{-i \pi /n }\) để tính DFT ngược, trong khi các tài liệu khác dùng \(e^{i \pi / n}\). Ở đây chúng ta có thể thấy dù là cách nào thì để thực hiện phép nhân nhanh đa thức modulo \(x^{2^t} + 1\) đều cần cả DFT thuận và ngược, nên kết quả không thay đổi nhờ vào tính chất của DFT.