Algoritmo fatorial mais eficiente que multiplicação ingênua

38

Eu sei como codificar fatoriais usando iterativo e recursivo (por exemplo, n * factorial(n-1)por exemplo). Li em um livro didático (sem maiores explicações) que há uma maneira ainda mais eficiente de codificar fatoriais, dividindo-os ao meio recursivamente.

Eu entendo por que esse pode ser o caso. No entanto, eu queria tentar codificá-lo por conta própria, e acho que não sei por onde começar. Um amigo sugeriu que eu escrevesse casos básicos primeiro. e eu estava pensando em usar matrizes para manter o controle dos números ... mas realmente não vejo como projetar esse código.

Que tipo de técnicas eu deveria estar pesquisando?

user65165
fonte

Respostas:

40

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.n1

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 n3 5 7 ( 2 n - 1 ) . n = 2 k k >n?135(2n1)(2n)!=123(2n)

(2n)!=n!2n357(2n1).
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)!=(2k1)!22k1(2k1)?( 2 k - 1 ) ? ( k - 2 ) + 2 k - 1 - 2 2 2 k - 2 2 2 k - 1
(2k)!=(22k1+2k2++20)i=0k1(2i)?=(22k1)i=1k1(2i)?.
(2k1)?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).(k2)+2k1222k222k1

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

András Salamon
fonte
Você deixou de fora um fator importante. O tempo de cálculo de acordo com o artigo de Borwein não é O (n log n log log n). É O (M (n log n) log log n), onde M (n log n) é o tempo para multiplicar dois números de tamanho n log n.
gnasher729
18

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:

  1. Organize os números a serem multiplicados (inicialmente, todos os números inteiros de a ) em dois conjuntos cujo produto seja aproximadamente do mesmo tamanho. Isso é muito mais barato do que fazer a multiplicação:(adição de uma máquina).n | um b | | a | + | b |1n|ab||a|+|b|
  2. Aplique o algoritmo recursivamente em cada um dos dois subconjuntos.
  3. Multiplique os dois resultados intermediários.

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 .n1n

¹ 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!

Gilles 'SO- parar de ser mau'
fonte
9

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!n171!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.

Yuval Filmus
fonte
"seus algoritmos iterativos e recursivos são equivalentes", você está se referindo à complexidade assintótica deles, certo? Quanto ao comentário no livro, estou traduzindo para outro idioma, talvez minha tradução seja uma porcaria.
user65165
O livro fala sobre iterativo e recursivo e, em seguida, comenta como se você usar divide e conquista para dividir n! ao meio você pode obter uma maneira mais rápida solução ...
user65165
11
Minha noção de equivalência não é completamente formal, mas você poderia dizer que as operações aritméticas executadas são as mesmas (se você alternar a ordem dos operandos no algoritmo recursivo). Um algoritmo "inerentemente" diferente executará um cálculo diferente, talvez usando algum "truque".
Yuval Filmus
11
Se você considerar o tamanho do número inteiro como um parâmetro na complexidade da multiplicação, a complexidade geral poderá mudar mesmo se as operações aritméticas forem "iguais".
Tpecatte
11
@CharlesOkwuagwu Certo, você poderia usar uma tabela.
Yuval Filmus