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 lcmfoi 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 -O2entã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 naumenta. Portanto, não é o gargalo, então podemos ignorá-lo por enquanto.
Isso significa que estamos realmente comparando a chamada em lcmque um argumento vai de 1 para ne 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 lcmrequer aplicações repetidas de mode 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.