Я изначально не планировал писать ответ. Но мне сказали, что после того, как другой пользователь сделал странное утверждение, что просто умножение простых чисел первой пары было более вычислительно дорогим, чем повторное применение lcm
. Итак, вот два алгоритма и некоторые тесты:
Мой алгоритм:
Алгоритм генерации простых чисел, дающий мне бесконечный список простых чисел.
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
Теперь используем этот простой список для вычисления результата для некоторых N
:
solvePrime :: Integer -> Integer
solvePrime n = foldl' (*) 1 $ takeWhile (<= n) (fromIntegral <$> primes)
Теперь другой основанный на lcm алгоритм, который, по общему признанию, является довольно лаконичным, главным образом потому, что я реализовал простое генерирование с нуля (и не использовал алгоритм понимания супер сжатого списка из-за его низкой производительности), тогда как он lcm
был просто импортирован из Prelude
.
solveLcm :: Integer -> Integer
solveLcm n = foldl' (flip lcm) 1 [2 .. n]
-- Much slower without `flip` on `lcm`
Теперь для тестов, код, который я использовал для каждого, был прост: ( -prof -fprof-auto -O2
тогда +RTS -p
)
main :: IO ()
main = print $ solvePrime n
-- OR
main = print $ solveLcm n
Для n = 100,000
, solvePrime
:
total time = 0.04 secs
total alloc = 108,327,328 bytes
против solveLcm
:
total time = 0.12 secs
total alloc = 117,842,152 bytes
Для n = 1,000,000
, solvePrime
:
total time = 1.21 secs
total alloc = 8,846,768,456 bytes
против solveLcm
:
total time = 9.10 secs
total alloc = 8,963,508,416 bytes
Для n = 3,000,000
, solvePrime
:
total time = 8.99 secs
total alloc = 74,790,070,088 bytes
против solveLcm
:
total time = 86.42 secs
total alloc = 75,145,302,416 bytes
Я думаю, что результаты говорят сами за себя.
Профилировщик указывает, что генерация простых чисел занимает все меньше и меньше процента времени выполнения по мере n
увеличения. Так что это не узкое место, поэтому мы можем пока игнорировать это.
Это означает, что мы действительно сравниваем вызов, lcm
где один аргумент идет от 1 до n
, а другой геометрически от 1 до ans
. Для звонков *
с такой же ситуацией и дополнительным преимуществом пропуска каждого непростого номера (асимптотически бесплатно, из-за более дорогой природы *
).
И хорошо известно, что *
это быстрее, чем lcm
, что lcm
требует многократного применения mod
, и mod
асимптотически медленнее ( O(n^2)
против ~O(n^1.5)
).
Таким образом, приведенные выше результаты и краткий анализ алгоритма должны сделать очень очевидным, какой алгоритм быстрее.