O melhor algoritmo conhecido é expressar o fatorial como um produto de potências primárias. Pode-se determinar rapidamente os números primos, bem como a potência correta para cada número primo usando uma abordagem de peneira. O cálculo de cada potência pode ser feito com eficiência usando o quadrado repetido e, em seguida, os fatores são multiplicados juntos. Isso foi descrito por Peter B. Borwein, Sobre a complexidade dos fatores de cálculo , Journal of Algorithms 6 376–380, 1985. ( PDF ) Em resumo,pode ser calculado no tempo , comparado ao tempo necessário ao usar a definição.n!O(n(logn)3loglogn)Ω(n2logn)
O significado do livro talvez fosse o método de dividir e conquistar. Pode-se reduzir as multiplicações usando o padrão regular do produto.n−1
Letdenote como uma notação conveniente. Reorganize os fatores de como
Agora suponha que para algum número inteiro . (Essa é uma suposição útil para evitar complicações na discussão a seguir, e a idéia pode ser estendida ao general .) Entãoe expandindo essa recorrência,
Computando1 ⋅ 3 ⋅ 5 ⋯ ( 2 n - 1 ) ( 2 n ) ! = 1 ⋅ 2 ⋅ 3 ⋯ ( 2 N ) ( 2 N ) ! = n ! ⋅ 2 n ⋅ 3 ⋅ 5 ⋅ 7 ⋯ ( 2 n - 1 ) . n = 2 k k >n?1⋅3⋅5⋯(2n−1)(2n)!=1⋅2⋅3⋯(2n)
(2n)!=n!⋅2n⋅3⋅5⋅7⋯(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)∏i=0k−1(2i)?=(22k−1)∏i=1k−1(2i)?.
(2k−1)?e multiplicar os produtos parciais em cada estágio leva multiplicações. Isso é uma melhoria de um fator de quase das multiplicações usando apenas a definição. Algumas operações adicionais são necessárias para calcular a potência de , mas na aritmética binária, isso pode ser feito de forma barata (dependendo do que é precisamente necessário, pode ser necessário adicionar apenas um sufixo de zeros).
( k - 2 ) + 2k - 1- 222k- 222k−1
O código Ruby a seguir implementa uma versão simplificada disso. Isso não evita a recálculo demesmo onde poderia fazê-lo: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)
Mesmo esse código de primeira passagem melhora no trivial
f = 1; (1..32768).map{ |i| f *= i }; print f
em cerca de 20% nos meus testes.
Com um pouco de trabalho, isso pode ser melhorado ainda mais, removendo também o requisito de que seja uma potência de (consulte a ampla discussão ).2n2
Lembre-se de que a função fatorial cresce tão rapidamente que você precisará de números inteiros de tamanho arbitrário para obter qualquer benefício de técnicas mais eficientes do que a abordagem ingênua. O fatorial de 21 já é grande demais para caber em um de 64 bits
unsigned long long int
.Até onde eu sei, não há algoritmo para calcular(fatorial de ), que é mais rápido que fazer as multiplicações.nn! n
No entanto, a ordem na qual você faz as multiplicações é importante. A multiplicação em um número inteiro da máquina é uma operação básica que leva o mesmo tempo, independentemente do valor do número inteiro. Mas para inteiros arbitrários de tamanho, o tempo que leva para multiplicar a e b depende do tamanho de um e b : um algoritmo ingênuo opera em tempo (onde é o número de dígitos de - em qualquer base que você quiser, pois o resultado é o mesmo até uma constante multiplicativa). Existem algoritmos de multiplicação mais rápidos , mas existe um limite inferior óbvio de| x | x Ω ( | a | + | b | ) max ( | a | , | b | )Θ(|a|⋅|b|) |x| x Ω(|a|+|b|) já que a multiplicação deve ler pelo menos todos os dígitos. Todos os algoritmos de multiplicação conhecidos crescem mais rapidamente do que linearmente em .max(|a|,|b|)
Armado com esse pano de fundo, o artigo da Wikipedia deve fazer sentido.
Como a complexidade das multiplicações depende do tamanho dos números inteiros que estão sendo multiplicados, você pode economizar tempo organizando as multiplicações em uma ordem que mantenha os números multiplicados pequenos. Funciona melhor se você organizar os números aproximadamente do mesmo tamanho. A “divisão ao meio” a que seu livro se refere consiste na seguinte abordagem de dividir e conquistar para multiplicar um (multi) conjunto de números inteiros:
Veja o manual GMP para mais detalhes.
Existem métodos ainda mais rápidos que não apenas reorganizam os fatores de a mas dividem os números decompondo-os em sua fatoração primária e reorganizando o produto muito longo resultante de números inteiros na maior parte pequenos. Vou apenas citar as referências do artigo da Wikipedia: “Sobre a complexidade do cálculo de fatoriais” de Peter Borwein e implementações de Peter Luschny .n1 n
¹ Há maneiras mais rápidas de computação aproximações de, mas isso não está computando mais o fatorial, está computando uma aproximação dele.n!
fonte
Como a função fatorial cresce muito rapidamente, seu computador pode armazenar apenaspara relativamente pequeno . Por exemplo, um duplo pode armazenar valores de até. Então, se você quer um algoritmo realmente rápido para calcular o, basta usar uma tabela de tamanho .n 171 ! n ! 171n! n 171! n! 171
A questão se torna mais interessante se você estiver interessado em Ou na função (ou em ). Em todos esses casos (incluindo ), Eu realmente não entendo o comentário no seu livro.Γ log Γlog(n!) Γ logΓ n!
Como um aparte, seus algoritmos iterativos e recursivos são equivalentes (até erros de ponto flutuante), desde que você esteja usando a recursão final.
fonte