Algoritma faktorial lebih efisien daripada perkalian naif

38

Saya tahu bagaimana kode faktorial menggunakan iteratif dan rekursif (misalnya n * factorial(n-1)untuk misalnya). Saya membaca dalam sebuah buku teks (tanpa diberi penjelasan lebih lanjut) bahwa ada cara pengkodean yang lebih efisien untuk faktorial dengan membaginya menjadi setengah secara rekursif.

Saya mengerti mengapa itu mungkin terjadi. Namun saya ingin mencoba mengkodekannya sendiri, dan saya rasa saya tidak tahu harus mulai dari mana. Seorang teman menyarankan saya menulis base case terlebih dahulu. dan saya berpikir untuk menggunakan array sehingga saya dapat melacak angka-angka ... tapi saya benar-benar tidak bisa melihat jalan keluar untuk merancang kode seperti itu.

Teknik apa yang harus saya teliti?

pengguna65165
sumber

Jawaban:

40

Algoritma terbaik yang dikenal adalah untuk mengekspresikan faktorial sebagai produk kekuatan utama. Seseorang dapat dengan cepat menentukan bilangan prima serta kekuatan yang tepat untuk setiap bilangan prima menggunakan pendekatan saringan. Komputasi setiap daya dapat dilakukan secara efisien menggunakan kuadrat ulang, dan kemudian faktor-faktor tersebut dikalikan bersama. Ini dijelaskan oleh Peter B. Borwein, On Complexity of Calculating Factorials , Journal of Algorithms 6 376-380, 1985. ( PDF ) Singkatnya, dapat dihitung dalam waktu O ( n ( log n ) 3 log log n ) , dibandingkan dengan Ω ( nn!HAI(n(logn)3loglogn) waktu yang diperlukan saat menggunakan definisi.Ω(n2logn)

Apa yang mungkin dimaksud buku teks adalah metode membagi dan menaklukkan. Satu dapat mengurangi perkalian dengan menggunakan pola reguler dari produk.n-1

Biarkan menyatakan 1 3 5 ( 2 n - 1 ) sebagai notasi yang mudah. Susun ulang faktor-faktor ( 2 n ) ! = 1 2 3 ( 2 n ) sebagai ( 2 n ) ! = n ! 2 n3 5 7 ( 2 n -n?135(2n-1)(2n)!=123(2n) Sekarang anggaplah n = 2 k untuk beberapa bilangan bulat k > 0 . (Ini adalah asumsi yang berguna untuk menghindari komplikasi dalam diskusi berikut, dan idenya dapat diperluas ke n umum.) Kemudian ( 2 k ) ! = ( 2 k - 1 ) ! 2 2 k - 1 ( 2 k - 1 ) ? dan dengan memperluas pengulangan ini, ( 2 k ) ! =

(2n)!=n!2n357(2n-1).
n=2kk>0n(2k)!=(2k-1)!22k-1(2k-1)? Komputasi(2k-dan mengalikan produk parsial pada setiap tahap membutuhkan(k-2)+2k-1
(2k)!=(22k-1+2k-2++20)saya=0k-1(2saya)?=(22k-1)saya=1k-1(2saya)?.
(2k-1)? perkalian. Ini merupakan peningkatan faktor hampir 2 dari 2 k - 2 perkalian hanya menggunakan definisi. Beberapa operasi tambahan diperlukan untuk menghitung kekuatan 2 , tetapi dalam aritmatika biner ini dapat dilakukan dengan murah (tergantung pada apa yang dibutuhkan secara tepat, mungkin hanya perlu menambahkan akhiran 2 k - 1 nol).(k-2)+2k-1-222k-222k-1

Kode Ruby berikut mengimplementasikan versi yang disederhanakan ini. Ini tidak menghindari menghitung ulang bahkan di mana ia bisa melakukannya: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)

Bahkan kode first-pass ini meningkat pada hal yang sepele

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

sekitar 20% dalam pengujian saya.

Dengan sedikit kerja, ini dapat ditingkatkan lebih lanjut, juga menghilangkan persyaratan bahwa menjadi kekuatan 2 (lihat diskusi ekstensif ).n2

András Salamon
sumber
Anda mengabaikan faktor penting. Waktu perhitungan menurut makalah Borneo bukanlah O (n log n log log n). Ini adalah O (M (n log n) log log n), di mana M (n log n) adalah waktu untuk mengalikan dua angka ukuran n log n.
gnasher729
18

Ingatlah bahwa fungsi faktorial tumbuh begitu cepat sehingga Anda membutuhkan bilangan bulat berukuran sembarang untuk mendapatkan manfaat dari teknik yang lebih efisien daripada pendekatan naif. Faktorial 21 sudah terlalu besar untuk bisa dimasukkan dalam 64-bitunsigned long long int .

n!n

Namun, urutan di mana Anda melakukan perkalian penting. Perkalian pada integer mesin adalah operasi dasar yang membutuhkan waktu yang sama tidak peduli berapa nilai integer tersebut. Tetapi untuk bilangan bulat berukuran sewenang-wenang, waktu yang diperlukan untuk mengalikan a dan b tergantung pada ukuranΘ(|Sebuah||b|)|x|xΩ(|Sebuah|+|b|)maks(|Sebuah|,|b|)

Berbekal latar belakang ini, artikel Wikipedia harus masuk akal.

Karena kompleksitas penggandaan bergantung pada ukuran bilangan bulat yang sedang dikalikan, Anda dapat menghemat waktu dengan mengatur perkalian dalam urutan yang membuat angka dikalikan kecil. Ini bekerja lebih baik jika Anda mengatur agar jumlahnya kira-kira berukuran sama. "Pembagian dalam setengah" yang mengacu pada buku teks Anda terdiri dari pendekatan pembagian dan penakluk berikut untuk melipatgandakan serangkaian bilangan bulat:

  1. 1n|Sebuahb||Sebuah|+|b|(satu penambahan mesin).
  2. Terapkan algoritma secara rekursif pada masing-masing dua himpunan bagian.
  3. Lipat gandakan dua hasil antara.

Lihat manual GMP untuk lebih spesifik.

1n

n!

Gilles 'SANGAT berhenti menjadi jahat'
sumber
9

n!n171!n!171

log(n!)ΓlogΓn!

Selain itu, algoritma iteratif dan rekursif Anda setara (hingga kesalahan floating point), karena Anda menggunakan rekursi ekor.

Yuval Filmus
sumber
"Algoritma iteratif dan rekursif Anda setara" Anda mengacu pada kompleksitas asimptotiknya, bukan? Adapun komentar di buku teks, baik saya menerjemahkannya dari bahasa lain jadi, mungkin terjemahan saya menyebalkan.
user65165
Buku ini berbicara tentang berulang dan rekursif, dan kemudian mengomentari bagaimana jika Anda menggunakan membagi dan menaklukkan untuk membagi n! setengah Anda bisa mendapatkan solusi cara yang lebih cepat ...
user65165
1
Gagasan saya tentang kesetaraan tidak sepenuhnya formal, tetapi Anda bisa mengatakan bahwa operasi aritmatika yang dilakukan adalah sama (jika Anda mengganti urutan operan dalam algoritma rekursif). Algoritme berbeda "inheren" akan melakukan perhitungan yang berbeda, mungkin menggunakan beberapa "trik".
Yuval Filmus
1
Jika Anda menganggap ukuran bilangan bulat sebagai parameter dalam kompleksitas perkalian, kompleksitas keseluruhan dapat berubah bahkan jika operasi aritmatika "sama".
Tpecatte
1
@CharlesOkwuagwu Benar, Anda bisa menggunakan meja.
Yuval Filmus