Algoritmo fattoriale più efficiente della moltiplicazione ingenua


38

So codificare per fattoriali usando sia iterativi che ricorsivi (ad n * factorial(n-1)es. Per es.). Ho letto in un libro di testo (senza ulteriori spiegazioni) che esiste un modo ancora più efficiente di codificare i fattoriali dividendoli a metà in modo ricorsivo.

Capisco perché potrebbe essere così. Tuttavia, volevo provare a codificarlo da solo, e non credo di sapere da dove iniziare. Un amico mi ha suggerito di scrivere prima i casi base. e stavo pensando di usare array in modo da poter tenere traccia dei numeri ... ma non riesco davvero a trovare una via d'uscita per progettare un tale codice.

Che tipo di tecniche dovrei ricercare?

Risposte:


40

L'algoritmo migliore che si conosca è quello di esprimere il fattoriale come prodotto dei poteri principali. È possibile determinare rapidamente i numeri primi e la giusta potenza per ciascun numero primo usando un approccio a setaccio. Il calcolo di ogni potenza può essere eseguito in modo efficiente utilizzando la quadratura ripetuta, quindi i fattori vengono moltiplicati insieme. Questo è stato descritto da Peter B. Borwein, On the Complexity of Calculating Factorials , Journal of Algorithms 6 376–380, 1985. ( PDF ) In breve, n!può essere calcolato in O(n(logn)3loglogn) tempo, rispetto al tempo richiesto quando si utilizza la definizione.Ω(n2logn)

Ciò che il libro di testo forse significava era il metodo di divisione e conquista. Si possono ridurre le moltiplicazioni usando il modello regolare del prodotto.n1

Let denotare 1 3 5 ( 2 n - 1 ) come notazione conveniente. Riorganizza i fattori di ( 2 n ) ! = 1 2 3 ( 2 n ) come ( 2 n ) ! = n ! 2 n3 5 7 ( 2 n -n?135(2n1)(2n)!=123(2n) Supponiamo ora n = 2 k per un numero intero k > 0 . (Questo è un presupposto utile per evitare complicazioni nella discussione che segue, e l'idea può essere estesa al n generale.) Quindi ( 2 k ) ! = ( 2 k - 1 ) ! 2 2 k - 1 ( 2 k - 1 ) ? e espandendo questa ricorrenza, ( 2 k ) ! =

(2n)!=n!2n357(2n1).
n=2kk>0n(2k)!=(2k1)!22k1(2k1)? Informatica( 2 k - 1 )?
(2k)!=(22k1+2k2++20)i=0k1(2i)?=(22k1)i=1k1(2i)?.
(2k1)?e moltiplicare i prodotti parziali in ciascuna fase richiede moltiplicazioni. Questo è un miglioramento di un fattore di quasi 2 da 2 k - 2 moltiplicazioni usando solo la definizione. Sono necessarie alcune operazioni aggiuntive per calcolare la potenza di 2 , ma nell'aritmetica binaria ciò può essere fatto in modo economico (a seconda di ciò che è esattamente richiesto, potrebbe essere necessario aggiungere un suffisso di 2 k - 1 zero).(k2)+2k1222k222K-1

Il seguente codice Ruby implementa una versione semplificata di questo. Questo non evita di ricalcolare anche dove potrebbe farlo: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)

Anche questo codice di primo passaggio migliora il banale

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

di circa il 20% nei miei test.

Con un po 'di lavoro, questo può essere ulteriormente migliorato, eliminando anche la necessità che sia una potenza di 2 (vedere la discussione approfondita ).n2


Hai lasciato fuori un fattore importante. Il tempo di calcolo secondo il documento di Borwein non è O (n log n log log n). È O (M (n log n) log log n), dove M (n log n) è il tempo per moltiplicare due numeri di dimensioni n log n.
gnasher729,

18

Tieni presente che la funzione fattoriale cresce così velocemente che avrai bisogno di numeri interi di dimensioni arbitrarie per ottenere qualsiasi beneficio da tecniche più efficienti rispetto all'approccio ingenuo. Il fattoriale di 21 è già troppo grande per adattarsi a un 64-bit unsigned long long int.

n!n

Θ(|un'||B|)|X|XΩ(|un'|+|B|)max(|un'|,|B|)

Con questo sfondo, l' articolo di Wikipedia dovrebbe avere un senso.

Poiché la complessità delle moltiplicazioni dipende dalla dimensione degli interi che si stanno moltiplicando, è possibile risparmiare tempo organizzando le moltiplicazioni in un ordine che mantiene i numeri moltiplicati piccoli. Funziona meglio se organizzi che i numeri abbiano approssimativamente le stesse dimensioni. La "divisione a metà" a cui fa riferimento il tuo libro di testo consiste nel seguente approccio di divisione e conquista per moltiplicare un (multi) insieme di numeri interi:

  1. 1n|un'B||un'|+|B|
  2. Applicare l'algoritmo in modo ricorsivo su ciascuno dei due sottoinsiemi.
  3. Moltiplica i due risultati intermedi.

Vedi il manuale GMP per maggiori dettagli.

1n

n!


9

n!n171!n!171

log(n!)ΓlogΓn!

A parte questo, i tuoi algoritmi iterativi e ricorsivi sono equivalenti (fino a errori in virgola mobile), poiché stai usando la ricorsione della coda.


"i tuoi algoritmi iterativi e ricorsivi sono equivalenti" ti riferisci alla loro complessità asintotica, giusto? per quanto riguarda il commento nel libro di testo, lo sto traducendo da un'altra lingua, quindi forse la mia traduzione fa schifo.
user65165

Il libro parla di iterativo e ricorsivo, e poi commenta come se si usa divide and conquer per dividere n! a metà puoi ottenere una soluzione molto più veloce ...
user65165

1
La mia nozione di equivalenza non è del tutto formale, ma si potrebbe dire che le operazioni aritmetiche eseguite sono le stesse (se si cambia l'ordine degli operandi nell'algoritmo ricorsivo). Un algoritmo "intrinsecamente" diverso eseguirà un calcolo diverso, magari usando qualche "trucco".
Yuval Filmus,

1
Se si considera la dimensione dell'intero come un parametro nella complessità della moltiplicazione, la complessità complessiva può cambiare anche se le operazioni aritmetiche sono "le stesse".
Tpecatte,

1
@CharlesOkwuagwu Bene, potresti usare una tabella.
Yuval Filmus
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.