Inicialmente, eu não estava pensando em escrever uma resposta. Mas fui informado de que depois que outro usuário fez a estranha afirmação de que simplesmente multiplicar os dois primeiros números primos era mais caro em termos de computação do que aplicar repetidamente lcm
. Então, aqui estão os dois algoritmos e alguns benchmarks:
Meu algoritmo:
Algoritmo de geração principal, fornecendo uma lista infinita de números primos.
isPrime :: Int -> Bool
isPrime 1 = False
isPrime n = all ((/= 0) . mod n) (takeWhile ((<= n) . (^ 2)) primes)
toPrime :: Int -> Int
toPrime n
| isPrime n = n
| otherwise = toPrime (n + 1)
primes :: [Int]
primes = 2 : map (toPrime . (+ 1)) primes
Agora, usando essa lista principal para calcular o resultado para alguns N
:
solvePrime :: Integer -> Integer
solvePrime n = foldl' (*) 1 $ takeWhile (<= n) (fromIntegral <$> primes)
Agora, o outro algoritmo baseado no lcm, que é bastante conciso, principalmente porque implementei a geração principal do zero (e não usei o algoritmo de compreensão da lista super concisa devido ao seu baixo desempenho), ao passo que lcm
foi simplesmente importado do Prelude
.
solveLcm :: Integer -> Integer
solveLcm n = foldl' (flip lcm) 1 [2 .. n]
-- Much slower without `flip` on `lcm`
Agora, para os benchmarks, o código que usei para cada um era simples: ( -prof -fprof-auto -O2
então +RTS -p
)
main :: IO ()
main = print $ solvePrime n
-- OR
main = print $ solveLcm n
Para n = 100,000
, solvePrime
:
total time = 0.04 secs
total alloc = 108,327,328 bytes
vs solveLcm
:
total time = 0.12 secs
total alloc = 117,842,152 bytes
Para n = 1,000,000
, solvePrime
:
total time = 1.21 secs
total alloc = 8,846,768,456 bytes
vs solveLcm
:
total time = 9.10 secs
total alloc = 8,963,508,416 bytes
Para n = 3,000,000
, solvePrime
:
total time = 8.99 secs
total alloc = 74,790,070,088 bytes
vs solveLcm
:
total time = 86.42 secs
total alloc = 75,145,302,416 bytes
Eu acho que os resultados falam por si.
O criador de perfil indica que a geração principal ocupa uma porcentagem cada vez menor do tempo de execução à medida que n
aumenta. Portanto, não é o gargalo, então podemos ignorá-lo por enquanto.
Isso significa que estamos realmente comparando a chamada em lcm
que um argumento vai de 1 para n
e o outro vai geometricamente de 1 para ans
. Telefonar *
com a mesma situação e o benefício adicional de pular todos os números não primos (assintoticamente de graça, devido à natureza mais cara *
).
E é sabido que *
é mais rápido do que lcm
, pois lcm
requer aplicações repetidas de mod
e mod
é assintoticamente mais lento ( O(n^2)
vs ~O(n^1.5)
).
Portanto, os resultados acima e a breve análise do algoritmo devem tornar muito óbvio qual algoritmo é mais rápido.