Phép nhân 2 số nguyên là một trong những bài toán lớn trong lĩnh vực Cryptography (Mật mã học). Nó ảnh hưởng trực tiếp đến hiệu suất của các thuật toán mã hoá, giải mã và các thuật toán khác. Nhân 2 số nguyên với n chữ số có thể được thực hiện với độ phức tạp O(nlog(n)log(log(n))) bằng cách sử dụng Fast Fourier Transform thay vì O(n2) bằng cách thông thường.
Kĩ thuật này sử dụng phương pháp Polynomial Multiplication (Nhân Đa thức) và Recursive Divide and Conquer (Chia Để Trị). Một trong những triển khai hiệu quả của Fast Fourier Transform trong việc tìm tích của 2 số nguyên lớn là thuật toán Schönhage–Strassen.
Giới thiệu
Như đã đề cập, việc nhân 2 số nguyên n chữ số yêu cầu độ phức tạp là O(n2), đây là độ phức tạp khổng lồ khi các thuật toán mã hoá thường xử lí các số nguyên lên đến 4096 bit. Do đó, con số tối đa là 24096−1 (gồm 4096 bit 1). Khi đó, độ dài của số nguyên này trong hệ thập phân sẽ là:
⌊log(10)log(24096−1)⌋+1=1234
Với con số tối thiểu 24096−1 (bit 1 đầu tiên và 4095 bit 0), độ dài của số nguyên này trong hệ thập phân gần như là tương đương:
⌊log(10)log(24096−1)⌋+1=1233
Điều này có nghĩa là độ dài 4096 trong hệ nhị phân sẽ tương đương với độ dài 1233 trong hệ thập phân. Đây là con số đủ lớn để độ phức tạp O(n2) trở thành một vấn đề cần giải quyết.
Phép nhân truyền thống
Để giải thích tại sao độ phức tạp của phép nhân 2 số nguyên theo cách thông thường là O(n2), ta sẽ xem xét một ví dụ cụ thể.
Cho 2 số nguyên 123 và 456, ta sẽ thực hiện phép nhân như sau:
Dù có thực hiện phương pháp gì, chúng ta cũng sẽ phải đi qua tổng cộng 3×3=9 bước tính. Cho nên độ phức tạp sẽ luôn là O(n2).
Polynomial Multiplication
Ta sẽ coi một số nguyên dương N là một đa thức P(x) thoả mãn:
P(x)=i=0∑n−1aixi
Trong đó:
x là base (cơ số) của N
a là coefficient vector (vector hệ số) của N.
Ví dụ, cho số nguyên 123456789, với base B=10, ta sẽ có coefficient vector a=[9,8,7,6,5,4,3,2,1]. Lúc này, đa thức P(x) sẽ là:
P(x)=9x0+8x1+7x2+6x3+5x4+4x5+3x6+2x7+1x8
Ngoài ra, với số nguyên trên:
Với base B=100, ta sẽ có a=[89,67,45,23,1]
Với base B=1000, ta sẽ có a=[789,456,123]
Với base B=16, ta sẽ có a=[5,1,13,12,11,5,7] (tương đương 0x51d0b57, là biểu diễn hexadecimal của 123456789 dưới dạng little-endian).
Nhưng tại sao chúng ta lại biểu diễn dưới dạng little-endian như vậy mà không đảo ngược lại (cho x8 ở vị trí đầu tiên)? Câu trả lời là việc biểu diễn least significant digit (chữ số hàng đơn vị) ở vị trí bit đầu tiên sẽ giúp máy tính tính toán nhanh hơn.
Ví dụ với phép cộng và trừ, chúng ta phải bắt đầu từ hàng đơn vị, sau đó lưu carry (nhớ) lại để cộng cho hàng tiếp theo. Việc di chuyển đến hàng chục, hàng trăm, hàng nghìn,... chỉ đơn giản là di chuyển đến phần tử tiếp theo của vector. Trong khi đó, cách biểu diễn big-endian sẽ buộc chúng ta phải di chuyển đến phần tử cuối cùng của vector trước rồi mới có thể lùi lại dần để tính. Hơn nữa, việc lưu kết quả cũng dễ hơn rất nhiều vì chúng ta lưu kết quả của phép tính từ phải sang trái.
Và quan trọng nhất, với little-endian, chúng ta có thể thoải mái resize vector (hay chèn các số 0 vào cuối vector) sẽ không ảnh hưởng đến giá trị số nguyên. Trong khi đó, với big-endian, việc thêm một số 0 sẽ làm số nguyên cộng thêm một lượng Bn. Dẫn đến việc vô cùng bất tiện khi resize mảng để thực hiện các phép nhân đa thức (vì chúng ta phải dịch toàn bộ các phần tử ra sau để chèn các số 0 vào các vị trí đầu tiên).
Thuật toán Fast Fourier Transform
Recursive Divide and Conquer
Chúng ta sẽ chia đa thức P(x) thành 2 đa thức con Peven(x) và Podd(x).
Trong đó:
Peven(x) là đa thức con của P(x) với các hệ số ở vị trí chẵn.
Podd(x) là đa thức con của P(x) với các hệ số ở vị trí lẻ.
Ví dụ, đa thức P(x)=a0+a1x+a2x2+⋯+an−1xn−1 sẽ được chia thành:
Cho số phức w là Primitive n-th Root of Unity (Căn Đơn vị Nguyên thủy bậc n). Ta định nghĩa n-th root of unity (căn đơn vị bậc n) là các số phức thoả phương trình wn−1=0. Trong đó tính primitive (nguyên thủy) cho biết các số phức w được coi là nghiệm khi và chỉ khi nó không phải là k-th root of unity với bất kì k<n.
Nghiệm của phương trình trên là một tập hợp các số phức X={w0,w1,…,wk−1} cách đều nhau trên một unit circle (đường tròn đơn vị) trong mặt phẳng phức. Để hiểu tại sao lại là n-th root (căn bậc n), đây là dạng đầy đủ của nghiệm wnk:
Algorithm: Thuật toaˊn Fast Fourier Transform (FFT)Input: coefficient vector a←[a0,a1,…,an−1]Procedure: FFT(a,invert)if n←1thenreturn end ifaeven←[a0,a2,a4,…,an−2]aodd←[a1,a3,a5,…,an−1]FFT(aeven),FFT(aodd)angle←2×π/nw←1,wn←cos(angle)+i×sin(angle)for k←0,1,…,n/2−1 doy[k]←aeven[k]+w×aodd[k]y[k+n/2]←aeven[k]−w×aodd[i]w←w×wnend forEnd Procedure
Độ phức tạp thuật toán Fast Fourier Transform
Sử dụng Master Theorem (Định lý Master), ta nhận thấy thuật toán chia ra làm 2 phần, mỗi phần có kích thước là n/2 còn lại có độ phức tạp là O(n), do đó độ phức tạp thời gian của thuật toán là:
T(n)=2×T(2n)+O(n)=O(nlog(n))
Thuật toán Schönhage–Strassen
Inverse Fast Fourier Transform
Cho 2 số nguyên viết dưới dạng đa thức là A(x) và B(x), khi đó:
Ta sẽ gọi FFT−1 là Inverse Fast Fourier Transform, có thuật toán tương tự như FFT, tuy nhiên FFT−1 sẽ có tác dụng chuyển đổi từ một vector các giá trị y về một coefficient vector a.
Hay nói cách khác FFT−1 giống như một phép interpolation (nội suy) tìm một đa thức thoả mãn các giá trị cho trước, bằng một vài phép biến đổi ma trận, ta sẽ có được công thức bên dưới:
ak=n1j=0∑n−1yj×wn−kjvới k=0,1,…,n−1
Tất nhiên độ dài vector giá trị y và độ dài coefficient vector a là bằng nhau và bằng n. Vì với n phần tử là đủ để xác định một đa thức bậc n−1. Đây là tính chất cơ bản trong phép interpolation.
Vì công thức trên hoàn toàn tương đồng với công thức của FFT, nên ta có thể sử dụng lại thuật toán FFT để tìm FFT−1. Trong đó, thay vì dùng wnk, ta sẽ dùng wn−k:
wn−k=cos(−n2πk)+i×sin(−n2πk)
Do đó, chúng ta sẽ gán angle=−2×π/n trong thuật toán. Sau khi có được coefficient vector A×B, ta sẽ thực hiện bước cuối cùng là chuẩn hoá về base B của số nguyên.