Chúng ta minh hoạ quá trình này với ví dụ đã sử dụng nhân 981 với 1234. Trước tiên, chúng ta điền thêm vào toán hạng ngắn hơn một số không vô nghĩa để làm nó có cùng độ dài như toán hạng dài hơn, vậy 981 trở thành 0981. Sau đó,chúng ta tách từng toán hạng thành hai nửa: 0981 cho ra w = 09 và x = 81, còn 1234 thành y = 12 và z = 34. Lưu ý rằng, 981 = 102w + x và 1234 = 102y + z. Do đó, tích cần tìm có thể tính được là:
981 × 1234 = (102w + x) × (102y + z) = 104wy + 102(wz + xy) + xz
= 1080000 + 127800 + 2754 = 1210554
Tuy nhiên, cách làm trên vẫn còn cần đến bốn phép nhân hai nửa: wy, wz, xy và zx.Để ý điểm mấu chốt ở đây là thực ra thì không cần tính cả wz lẫn xy, mà là tổng của hai số hạng này. Liệu có thể thu được wz + xy với chi phí của một phép nhân mà thôi hay không? Điều này có vẻ như không thể được cho đến khi chúng ta nhớ ra rằng mình cũng cần những giá trị wz và xy để đưa vào công thức trên. Xét tích:
r = (w + x)× (y + z) = wy + (wz + xy) + xz
Chỉ sau một phép nhân, chúng ta thu được tổng của tất cả ba số hạng cần thiết để tính được tích mình mong muốn. Điều này gợi ý một cách tiến hành như sau:
p = wy = 09× 12 = 108 q = xz = 81× 34 = 2754 r = (w + x)× (y + z) = 90× 46 = 4140 và cuối cùng 981× 1234 = 104p + 102(r – p – q) +q = 1080000 + 127800 + 2754 = 1210554
Như vậy, tích của 981 và 1234 có thể rút gọn về ba phép nhân của hai số có hai chữ số (09× 12, 81× 34 và 90× 46) cùng với một số nào đó phép dịch chuyển (nhân với luỹ thừa của 10), phép cộng và phép trừ.Chắc chắn là số các phép cộng coi phép trừ như là phép cộng –có nhiều hơn so với thuật toán “Chia để trị” nguyên thuỷ ở phần trên. Vậy thì có đáng để thực hiện bốn phép cộng nhiều hơn để tiết kiệm một phép nhân hay không? Câu trả lời là không nếu chúng ta đang nhân số nhỏ như những số trong ví dụ này. Tuy nhiên, sẽ là đáng giá nếu các số cần được nhân với nhau đủ lớn và chúng càng lớn thì lại càng đáng làm như vậy. Khi các số hạng đủ lớn, thời gian cần cho các phép cộng và dịch chuyểntrở thành bỏ qua được so với thời gian cần cho chỉ một phép nhân. Như vậy, là có
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
lý do để kỳ vọng rằng rút gọn bốn phép nhân về còn ba sẽ giúp chúng ta cắt giảm được 25% thời gian tính toán đòi hỏi cho việc nhân các số lớn. Như chúng ta sẽ thấy, sự tiết kiệm của mình sẽ tốt hơn một cách đáng kể.
Để giúp chúng ta hiểu rõ được những gì mình đạt được, hãy giả thiết rằng có một cài đặt của thuật toán nhân cổ điển đòi hỏi thời gian h(n) = cn2để nhân hai số có n chữ số, với hằng số c phụ thuộc vào cài đặt đó. (Ở đây ta có sự đơn giản hoá vì trên thực tế thì thời gian đòi hỏi còn có dạng phức tạp hơn, chẳng hạn như cn2+ bn + a). Tương tự, cho g(n) là thời gian mà thuật toán “Chia để trị” cần để nhân hai số n chữ số, không tính thời gian cần thiết để thực hiện ba phép nhân hai nửa. Nói cách khác, g(n) là thời gian cần thiết cho các phép cộng, dịch chuyển và các phép tính phụ thêm khác. Dễ dàng cài đặt các phép tính này sao cho g(n) Θ(n). Hãy tạm thời bỏ qua điều gì sẽ xảy ra nếu n lẻ và nếu các số hạng không có cùng độ dài.
Nếu từng trong số ba phép nhân hai nửa được thực hiện bằng thuật toán cổ điển, thời gian cần thiết để nhân hai số có n chữ số là:
3h(n/2) + g(n) = 3c(n/2)2+ g(n) = ¾ cn2+ g(n) = ¾ h(n) + g(n).
Vì h(n) Θ(n2) và g(n) Θ(n), số hạng g(n) là bỏ qua được so với ¾h(n) khi n đủ lớn, có nghĩa là chúng ta tăng được tốc độ lên khoảng 25% so với thuật toán cổ điển như đã mong đợi. Mặc dù sự cải thiện này là không thể xem thường được nhưng chúng ta vẫn không làm được thay đổi bậc của thời gian cần thiết: thuật toán mới vẫn cần thời gian tính bậc hai.
Để có thể làm được tốt hơn thế, chúng ta trở lại với câu hỏi đặt ra ở đoạn mở đầu: các bài toán con cần được giải như thế nào? Nếu chúng nhỏ thôi thì thuật toán cổ điển có thể vẫn còn là cách làm tốt nhất. Tuy nhiên, khi những bài toán con cũng đủ lớn, chẳng lẽ sử dụng thuật toán mới của chúng ta một cách đệ quy cũng không hơn gì hay sao? Ý tưởng này tương tự như hưởng lợi nhuận từ một tài khoản ngân hàng có gộp vốn lẫn lãi! Nếu chúng ta làm như vậy sẽ thu được một thuật toán có thể nhân hai số n chữ số trong một thời gian t(n) = 3t(n/2) + g(n) khi n chẵn và đủ lớn.
Vì lg3 ≈ 1.585 nhỏ hơn 2, thuật toán này có thể nhân hai số nguyên lớn nhanh hơn rất nhiều so với thuật toán nhân cổ điển và n càng lớn thì sự cải thiện này càng đáng giá. Một cài đặt tốt có thể không sử dụng cơ số 10, mà sử dụng cơ số lớn nhất để với cơ số đó phần cứng cho phép nhân trực tiếp hai chữ số với nhau. Một nhân tố quan trọng trong hiệu suất thực tế của cách tiếp cận phép nhân này và của bất kỳ thuật toán chia để trị nào là biết khi nào cần dừng việc phân chia các bài toán và thay vào đó sử dụng thuật toán cổ điển. Mặc dù cách tiếp cận chia để trị trở nên có ích khi bài toán cần giải đủ lớn, trên thực tế nó có thể chậm hơn so với thuật toán cổ điển đối với những bài toán quá nhỏ. Do đó thuật toán chia để trị phải tránh việc thực hiện đệ quy khi kích thước của các bài toán con không
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
phù hợp nữa. Để đơn giản, một số vấn đề quan trọng đến nay đã bị bỏ qua. Làm thế nào để chúng ta giải quyết được những số có độ dài lẻ? Mặc dù cả hai nửa của số nhân và số bị nhân đều có kích thước n/2, có thể xảy ra trường hợp tổng của chúng bị tràn và có kích thước vượt quá 1. Do đó, sẽ không hoàn toàn chính xác khi nói rằng r = (w+x)× (y+z)
bao hàm phép nhân hai nửa. Điều này ảnh hưởng tới việc phân tích thời gian chạy như thế nào? Làm thế nào để nhân hai số có kích thước khác nhau? Còn những phép tính số học nào khác với phép nhân mà ta có thể xử lý hiệu quả hơn so với dùng thuật toán cổ điển? Những số có độ dài lẻ được nhân dễ dàng bằng cách tách chúng càng gần ở giữa càng tốt: một số có n chữ số được tách thành một sốcó│n/2│chữ số và một số có│n/2│chữ số. Câu hỏi thứ hai còn khắt khe hơn. Xét nhân 5678 với 6789. Thuật toán của chúng ta tách các số hạng thành w = 56, x=78, y=67 và z = 89. Ba phép nhân hai nửa cần thực hiện là:
p = wy = 56× 67
q = xz = 78× 89 và
r = (w + x)× (y + z) = 134× 156
Phép nhân thứ ba bao gồm những số ba chữ số, do vậy nó không thực sự là một nửa so với phép nhân nguyên thuỷ của các số có bốn chữ số. Tuy nhiên kích thước của w + x
và y+z không thể vượt quá 1 +│n/2│.Để đơn giản hoá việc phân tích, cho T(n) là thời gian mà thuật toán này thực hiện trong tình huống xấu nhất để nhân hai số có kích thước tố đa là n (thay vì chính xác bằng n). Theo định nghĩa thì t(n) là một hàm không giảm. Khi n đủ lớn thuật toán của chúng ta rút gọn phép nhân hai số có kích thước tối đa n đó về ba phép nhân nhỏ hơn p = wy, q = xz và r = (w + x)× (y + z) với kích thước tối đa tương ứng là │n/2│,│n/2│và 1 +│n/2│, thêm vào đó là những thao tác đơn giản chiếm thời gian là O(n). Do đó, ở đây tồn tại hằng số dương c sao cho:
t(n) ≤ t(│n/2│) + t(│n/2│) + t(1 + │n/2│) + cn
với mọi n đủ lớn. Điều này chính xác là phép đệ quy cho kết quả giờ đây đã trở nên quen thuộc là T(n) Θ(nlg3
). Do vậy luôn luôn có thể nhân các số n chữ số với thời gian O(nlg3). Phân tích tình huống tồi nhất của thuật toán này chỉ ra rằng trên thực tế T(n) Θ(nlg3
), nhưng điều này không được quan tâm lắm vì còn có những thuật toán nhân nhanh hơn.
Quay lại với câu hỏi nhân các số có kích thước khác nhau, giả sử u và v là những số nguyên có kích thước tương ứng là m và n. Nếu m và n nằm trong khoảng đến hai lần của nhau, tốt nhất là điền vào số hạng nhỏ hơn những số 0 vô nghĩa để làm cho nó có cùng độ dài như số hạng kia, như chúng ta đã làm khi nhân 981 với 1234. Tuy nhiên, cách tiếp cận này không được khuyến khích khi một số hạng lớn hơn số hạng kia rất nhiều. Thậm chí nó có thể tồi hơn là dùng thuật toán nhân cổ điển!
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Không làm mất đi tính tổng quát, giả sử rằng m≤ n. Thuật toán chia để trị sử dụng điểm số và thuật toán cổ điển có thời gian tương ứng là Θ(nlg3)và Θ(mn) để tính các tích u và v. Xét thấy rằng hằng số ẩn của biểu thức trước có vẻ lớn hơn của biểu thức sau, chúng ta thấy rằng chia để trị sử dụng điền số chậm hơn thuật toán cổ điển khi m ≤
nlg3(3/2)và như vậy trường hợp đặc biệt khi m ≤ n1/2.Mặc dù vậy, rất dễ dàng kết hợp cả hai thuật toán để thu được một thuật toán thực sự tốt hơn. Ý tưởng là cắt lát số hạng dài hơn v thành những đoạn có kích thước m và sử dụng thuật toán chia để trị để nhân u với từng đoạn của v sao cho thuật toán chia để trị được dùng để nhân những cặp số hạng có cùng kích thước. Tích cuối cùng của u và v sau đó thu được dễ dàng bằng các phép cộng và dịch chuyển đơn giản. Thời gian chạy tổng cộng chủ yếu được dùng để thực hiện │n/m│ phép nhân các số m chữ số. Vì mỗi phép nhân nhỏ hơn này chiếm thời gian Θ(mlg3) và vì │n/m│ Θ(n/m), thời gian chạy tổng cộng để nhân một số n chữ số với một chữ số m chữ số là Θ(nmlg(3/2)) khi m ≤ n.
Phép nhân không chỉ là phép tính được quan tâm đối với số nguyên lớn. Phép luỹ thừa đồng dư là cốt yếu đối với khoa học mật mã hiện đại. Chia số nguyên các phép lấy phần dư và phép tính phần nguyên của căn bậc hai có thể được thực hiện với thời gian cùng bậc với phép nhân. Các phép tính quan trọng khác như tính ước số chung lớn nhất có thể còn khó hơn nữa.
3.6.3Mô hình thuật toán chia để trị cho bài toán nhân hai số nguyên lớn
Việc thực hiện nhân các số nguyên lớn không chỉ là ví dụ đơn lẻ của lợi ích tiếp cận phương pháp chia để trị. Xét một bài toán nào đó và dùng một giải thuật đơn giản xem liệu có khả năng giải quyết bài toánnày không. Chúng ta đặt ra câu hỏi: giải thuật đơn giản đó có hiệu quả trong những ví dụ nhỏhay không ? Chúng ta gọi đó là giải thuật con cơ bản (gtccb). Giải thuật nhân cổ điển chính là ví dụ của giải thuật con cơ bản.
Mô hình chung cho giải thuật chia và trị như sau:
Thuật toán DC
{
If (x hiệu quả với số nhỏ và đơn giản) return gtccb(x);
Phân tích x thành các số nhỏ hơn x1, x2, …, xp; For i = 1 to p do
yi = DC(xi);
Kết hợp lại các số yi để tạo được giải thuật y cho x Return y;
}
Số bài toán con n thường là nhỏ và độc lập với các ví dụ cụ thể cần giải quyết. Khi p=1, nó không phải phán đoán nhiều về việc “phân tích x thành các số nhỏ hơn x1” và khó có thể gọi đây là kỹ thuật chia để trị. Tuy nhiên, nó có khả năng làm giải pháp cho số lớn
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
thành giải pháp cho số nhỏ. Trong trường hợp này chia để trị còn được gọi là đơn giản hoá. Phép đơn giản hoá, đôi lúc có thể thay thế phép đệ quy trong chia và trị bằng cách dùng các phép phân tích lặp.Các giải thuật phân tích lặp thực thi trong ngôn ngữ lập trình trên các máy tính có cấu hình thấp thì có vẻ nhanh hơn mặc dù thực hiện nhân các hằng số. Nói một cách khác, làm theo cách này nó có thể tiết kiệm đáng kể phần lưu trữ, ví dụ với số có kích thước n, giải thuật đệ quy dùng một ngăn xếp có độ sâu trung bình là Ω(lg n) và trong tình huống tồi là Ω(n).
Để giải thuật chia và trị có hiệu quả cần phải đạt được các điều kiện: Quyết định khi dùng giải thuật con cơ bản tốt hơn gọi hàm đệ quy, nó phải phân tích các trường hợp thành những trường hợp con và kết hợp các giải pháp con lại với nhau cho hiệu quả và các trường hợp con có kích thước càng chênh lệch nhau càng tốt. Phần lớn các giải thuật chia để trị có số trường hợp con k được làm tròn từ n/b với b là hằng số, trong đó n là kích thước của trường hợp gốc.
Ví dụ, giải thuật chia và trị đối với phép nhân hai số lớn cần phải tốn θ(n) để phân tích bài toán gốc thành 3 bài toán con có kích thước bằng một nửa và kết hợp các kết quả lại với nhau: p = 3 và b = 2.
Xem g(n) là thời gian yêu cầu bởi DC đối với bài toán kích thước n, không tính thời gian cần cho gọi đệ quy. Tổng thời gian T(n) được thực hiện bởi giải thuật chia để trị có thể xem T(n) = p*T(n/b) + g(n) với n đủ lớn. Nếu ở đây tồn tại số nguyên k như g(n)
θ(nk ) ta có thể kết luận: log log l b k k k n neu p b T n n n n
Vấn đề còn lại là khi nào thì quyết định chia một bài toán và gọi hàm đệ quy hoặc xem bài toán là đơn giản và vận dụng trực tiếp bài toán con cơ bản. Mặc dù lựa chọn này không ảnh hưởng tới thời gian thực hiện giải thuật nhưng chúng ta cũng phải xét việc nhân các hằng số đã ẩn những giá trị nhỏ nhất có thể. Với phần lớn các giải thuật chia để trị, quyết định này dựa trên giải thuật đơn giản là lấy ngưỡng, và thường biểu thị n0. Giải thuật con cơ bản được dùng để giải quyết bất kỳ bài toán nào cho kích thước không quá n0. Chọn lựa ngưỡng tối ưu là rất phức tạp bởi vì giá trị tối ưu không chỉ liên quan tới giải thuật mà còn phụ thuộc vào việc thực thi cụ thể. Hơn nữa, cũng không có giải thuật chung nào để tìm ngưỡng tối ưu. Với một trường hợp cụ thể, ngưỡng tối ưu có thể quyết định bằng kinh nghiệm.Thuật toán được viết dạng giả mã như sau:
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/ DivideConquer(A, B, n) { if (n ==1) return A1*B1; k = n / 2; X =A(1..k); Y = A(k+1..n); U =B(1..k); V = B(k+1..n); P = Mul(X,Y,k); Q = Mul(X,Y,k); R = Mul(X+U,Y+V,k); T = P shl n + (R-P-Q) shl k + Q; return T; };
Hình 3.12Thuật toán nhân chia để trị
3.6.4So sánhđộ phức tạp giữa các thuật toán
Như đã phân tích ở trên, có rất nhiều thuật toán khác nhau được đề xuất giải quyết bài toán nhân hai số nguyên lớn. Mỗi thuật toán có ưu, nhược điểm riêng. Bảng 3.1 minh họa việc so sánh độ phức tạp giữa các thuật toán như đã phân tích.
Bảng 3.1 So sánh độ phức tạp tính toán của các thuật toán nhân
Tên thuật toán Độ phức tạp
Thuật toán nhân tự nhiên O(n2)
Thuật toán nhân cơ bản O(n2)
Thuật toán nhân Karatsuba-Ofman O(n1.58) Thuật toán nhân dựa trên biến đổi FFT O(nlon log logn)
Thuật toán nhân dựa trên chia để trị
log log l b k k k n neu p b T n n n n