Thuật toán nhân tố hiệu quả hơn phép nhân ngây thơ


38

Tôi biết cách viết mã cho các giai thừa bằng cách sử dụng cả lặp và đệ quy (ví n * factorial(n-1)dụ: ví dụ). Tôi đọc trong sách giáo khoa (không được đưa ra bất kỳ lời giải thích nào nữa) rằng có một cách mã hóa thậm chí còn hiệu quả hơn cho các giai thừa bằng cách chia chúng một nửa theo cách đệ quy.

Tôi hiểu tại sao đó có thể là trường hợp. Tuy nhiên tôi muốn tự mình thử mã hóa nó và tôi không nghĩ mình biết bắt đầu từ đâu. Một người bạn đề nghị tôi viết trường hợp cơ bản trước. và tôi đã nghĩ đến việc sử dụng các mảng để tôi có thể theo dõi các con số ... nhưng tôi thực sự không thể tìm ra cách nào để thiết kế một mã như vậy.

Những loại kỹ thuật tôi nên được nghiên cứu?

Câu trả lời:


40

Thuật toán tốt nhất được biết đến là thể hiện giai thừa như một sản phẩm của các quyền lực chính. Người ta có thể nhanh chóng xác định các số nguyên tố cũng như công suất phù hợp cho từng số nguyên tố bằng cách sử dụng phương pháp sàng. Việc tính toán từng công suất có thể được thực hiện một cách hiệu quả bằng cách sử dụng bình phương lặp đi lặp lại, và sau đó các yếu tố được nhân lên với nhau. Điều này đã được Peter B. Borwein mô tả, về sự phức tạp của các yếu tố tính toán , Tạp chí thuật toán 6 376 Điện 380, 1985. ( PDF ) Tóm lại,có thể được tính trong thời gian , so với thời gian cần thiết khi sử dụng định nghĩa.n!Ôi(n(đăng nhậpn)3đăng nhậpđăng nhậpn)Ω(n2đăng nhậpn)

Điều mà sách giáo khoa có lẽ có nghĩa là phương pháp phân chia và chinh phục. Người ta có thể giảm các phép nhân bằng cách sử dụng mẫu thông thường của sản phẩm.n-1

Đểbiểu thị là một ký hiệu thuận tiện. Sắp xếp lại các yếu tố của là Bây giờ giả sử với một số nguyên . (Đây là một giả định hữu ích để tránh các biến chứng trong cuộc thảo luận sau đây và ý tưởng có thể được mở rộng thành chung .) Sau đóvà bằng cách mở rộng sự tái phát này, Máy tính1 3 5 ( 2 n - 1 ) ( 2 n ) ! = 1 2 3 ( 2 n ) ( 2 n ) ! = n ! 2 n3 5 7 ( 2 n - 1 ) . n = 2 k k >n?135(2n-1)(2n)!= =123(2n)

(2n)!= =n!2n357(2n-1).
n= =2kn ( 2 k ) ! = ( 2 k - 1 ) ! 2 2 k - 1 ( 2 k - 1 ) ? ( 2 k ) ! = ( 2 2 k - 1 + 2 k - 2 + + 2 0 ) k - 1 i = 0 ( 2 i )k>0n(2k)!= =(2k-1)!22k-1(2k-1)?( 2 k - 1 ) ? ( k - 2 ) + 2 k - 1 - 2 2 2 k - 2 2 2 k - 1
(2k)!= =(22k-1+2k-2++20)Πtôi= =0k-1(2tôi)?= =(22k-1)Πtôi= =1k-1(2tôi)?.
(2k-1)?và nhân các sản phẩm một phần ở mỗi giai đoạn mất phép nhân. Đây là một cải tiến của hệ số gần từ phép nhân chỉ bằng cách sử dụng định nghĩa. Một số thao tác bổ sung được yêu cầu để tính toán sức mạnh của , nhưng trong số học nhị phân, điều này có thể được thực hiện với giá rẻ (tùy thuộc vào chính xác những gì được yêu cầu, nó có thể chỉ cần thêm một hậu tố số 0).(k-2)+2k-1-222k-222k-1

Mã Ruby sau đây thực hiện một phiên bản đơn giản hóa này. Điều này không tránh tính toán lạingay cả khi nó có thể làm như vậy:n?

def oddprod(l,h)
  p = 1
  ml = (l%2>0) ? l : (l+1)
  mh = (h%2>0) ? h : (h-1)
  while ml <= mh do
    p = p * ml
    ml = ml + 2
  end
  p
end

def fact(k)
  f = 1
  for i in 1..k-1
    f *= oddprod(3, 2 ** (i + 1) - 1)
  end
  2 ** (2 ** k - 1) * f
end

print fact(15)

Ngay cả mã đầu tiên này cũng cải thiện tầm thường

f = 1; (1..32768).map{ |i| f *= i }; print f

khoảng 20% ​​trong thử nghiệm của tôi.

Với một chút công việc, điều này có thể được cải thiện hơn nữa, đồng thời loại bỏ yêu cầu là sức mạnh của (xem phần thảo luận mở rộng ).2n2


Bạn đã bỏ qua một yếu tố quan trọng. Thời gian tính toán theo giấy của Borwein không phải là O (n log n log log n). Đó là O (M (n log n) log log n), trong đó M (n log n) là thời gian để nhân hai số có kích thước n log n.
gnasher729

18

Hãy nhớ rằng hàm giai thừa phát triển nhanh đến mức bạn sẽ cần các số nguyên có kích thước tùy ý để nhận được bất kỳ lợi ích nào của các kỹ thuật hiệu quả hơn so với phương pháp ngây thơ. Giai thừa của 21 đã quá lớn để phù hợp với 64 bit unsigned long long int.

Theo tôi biết, không có thuật toán để tính(giai thừa của ) nhanh hơn so với thực hiện phép nhân.¹nn!n

Tuy nhiên, thứ tự mà bạn thực hiện phép nhân. Phép nhân trên số nguyên máy là một thao tác cơ bản mất cùng thời gian bất kể giá trị của số nguyên là bao nhiêu. Nhưng đối với các số nguyên có kích thước tùy ý, thời gian cần để nhân ab phụ thuộc vào kích thước của ab : thuật toán ngây thơ hoạt động theo thời gian (trong đó là số chữ số của - trong bất kỳ cơ sở nào bạn thích, vì kết quả là giống nhau đến hằng số nhân). Có các thuật toán nhân nhanh hơn , nhưng có giới hạn dưới rõ ràng củaΘ(|a||b|)|x|xΩ(|a|+|b|)vì phép nhân ít nhất phải đọc tất cả các chữ số. Tất cả các thuật toán nhân đã biết tăng nhanh hơn tuyến tính trong .max(|a|,|b|)

Được trang bị nền tảng này, bài viết Wikipedia nên có ý nghĩa.

Do độ phức tạp của phép nhân phụ thuộc vào kích thước của số nguyên đang được nhân, nên bạn có thể tiết kiệm thời gian bằng cách sắp xếp các phép nhân theo thứ tự giữ cho các số được nhân nhỏ. Nó hoạt động tốt hơn nếu bạn sắp xếp cho các số có cùng kích thước. Bộ phận trong một nửa số mà cuốn sách giáo khoa của bạn đề cập đến bao gồm cách tiếp cận chia và chinh phục sau đây để nhân một bộ số nguyên (nhiều):

  1. Sắp xếp các số sẽ được nhân (ban đầu, tất cả các số nguyên từ đến ) trong hai bộ có sản phẩm có cùng kích thước. Điều này ít tốn kém hơn nhiều so với thực hiện phép nhân:(thêm một máy).1n|ab||a|+|b|
  2. Áp dụng thuật toán đệ quy trên mỗi trong hai tập con.
  3. Nhân hai kết quả trung gian.

Xem hướng dẫn sử dụng GMP để biết thêm chi tiết.

Thậm chí còn có các phương pháp nhanh hơn, không chỉ sắp xếp lại các yếu tố đến mà còn phân chia các số bằng cách phân tách chúng thành thừa số nguyên tố của chúng và sắp xếp lại sản phẩm rất dài của các số nguyên nhỏ. Tôi sẽ chỉ trích dẫn các tài liệu tham khảo từ bài viết trên Wikipedia: về sự phức tạp của việc tính toán các yếu tố của Peter Borwein và các triển khai của Peter Luschny .1n

¹ Có nhiều cách nhanh hơn máy tính xấp xỉ của, nhưng đó không phải là tính toán giai thừa nữa, nó tính toán gần đúng với nó.n!


9

Vì chức năng giai thừa phát triển quá nhanh, máy tính của bạn chỉ có thể lưu trữcho tương đối nhỏ . Ví dụ: một đôi có thể lưu trữ giá trị lên tới. Vì vậy, nếu bạn muốn một thuật toán thực sự nhanh chóng cho máy tính, chỉ cần sử dụng một bảng có kích thước .n!n171!n!171

Câu hỏi trở nên thú vị hơn nếu bạn quan tâm đến Hoặc trong chức năng (hoặc trong ). Trong tất cả các trường hợp này (bao gồm cả ), Tôi không thực sự hiểu nhận xét trong sách giáo khoa của bạn.đăng nhập(n!)Γđăng nhậpΓn!

Bên cạnh đó, các thuật toán lặp và đệ quy của bạn là tương đương (tối đa các lỗi dấu phẩy động), vì bạn đang sử dụng đệ quy đuôi.


"Các thuật toán lặp và đệ quy của bạn là tương đương" bạn đang đề cập đến độ phức tạp tiệm cận của chúng, phải không? Đối với nhận xét trong sách giáo khoa, tôi cũng đang dịch nó từ một ngôn ngữ khác vì vậy, có lẽ bản dịch của tôi rất tệ.
dùng65165

Cuốn sách nói về lặp đi lặp lại và đệ quy, và sau đó nhận xét về cách nếu bạn sử dụng phép chia và chinh phục để chia n! trong một nửa, bạn có thể nhận được một giải pháp nhanh hơn ...
user65165

1
Khái niệm tương đương của tôi không hoàn toàn chính thức, nhưng bạn có thể nói rằng các phép toán số học được thực hiện là như nhau (nếu bạn chuyển đổi thứ tự của toán hạng trong thuật toán đệ quy). Một thuật toán khác "vốn có" sẽ thực hiện một phép tính khác nhau, có thể sử dụng một số "mẹo".
Yuval Filmus

1
Nếu bạn coi kích thước của số nguyên là một tham số trong độ phức của phép nhân, thì độ phức tạp tổng thể có thể thay đổi ngay cả khi các phép toán số học là "giống nhau".
Tpecatte

1
@CharlesOkwuagwu Phải, bạn có thể sử dụng bảng.
Yuval Filmus
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.