【问题标题】:How to make this Haskell program run faster如何让这个 Haskell 程序运行得更快
【发布时间】:2021-06-19 07:52:02
【问题描述】:

所以我一直在尝试通过解决 Codeforce 上的一些问题来学习 Haskell。
即使我认为我的时间复杂度是最佳的,我也得到了很多 TLE(超出时间限制)。

我的问题是:我编写这个程序的方式是否让它变慢了?

例如,这里是problem

基本上答案是为给定的 n 找到 a<sub>n</sub> ,其中 <b>a<sub>n</sub></b> = 2*<b>a<sub>n-1</sub></b> + <b>D(n)</b><b>D(n)</b> = nn-1 之间的除数之差。

(更新:n 的上限为 106)。

下面是我的程序。

import qualified Data.Map.Strict as Map

main = do t <- read <$> getLine
          putStrLn . show $ solve t

solve :: Integer -> Integer
solve 0 = 1
solve 1 = 1
solve n = (2*(solve (n-1)) + (fact n) - (fact (n-1))) `mod` 998244353
    where fact n = foldl (\s -> \t -> s*(snd t + 1)) 1 (Map.toList . factorization  $ n)
    --the number of divisors of a number

--copied from Internet,infinite prime list
primes :: [Integer]
primes = 2: 3: sieve (tail primes) [5,7..]
  where
    sieve (p:ps) xs = h ++ sieve ps [x | x <- t, x `rem` p /= 0]
      where (h,~(_:t)) = span (< p*p) xs

--make factorization of a number
factorization :: Integer -> Map.Map Integer Integer
factorization 1 = Map.fromList []
factorization x = Map.insertWith (+) factor 1 (factorization (x `div` factor))
    where factor = head $ filter (\s -> (x `mod` s) == 0) ls
          ls = primes

这个程序未能在时限内解决。

那么谁能指出我做错了什么以及如何解决它?
或者在时间限制内使用 Haskell 解决这个问题是不可能的?

【问题讨论】:

  • 有一些方法可以改进您的代码,但我认为问题根本在于您选择的算法。我不是专家,但快速搜索会产生这篇文章:geeksforgeeks.org/count-divisors-n-on13。也许一个更简单但效率较低的算法仍然足够,例如:stackoverflow.com/a/15825953/15207568
  • 这个程序在取模之前处理非常大的数字。时间复杂度根本不是最优的。
  • 事实上,整个程序使用Int 很容易。
  • @Li-yaoXia 请问这对时间复杂度有什么影响?既然solve (n-1) 已经占用了module,而且只有两个小添加,那么它会比times 2,module 花费更多的时间吗? ,加法和模块?或者换句话说,操作大整数如何影响性能?
  • @Noughtmare n^(1/3) 方法令人兴奋。我陷入了自己的想法。感谢您的回复。

标签: haskell primes factors factorization


【解决方案1】:

您的时间复杂度并非最优的方式有很多。最明显的一个是使用试除法而不是例如筛子的质数查找器。也许这很好,因为您只计算了一次素数,但这并不能激发信心。

factorization 也至少有一个明显的问题。考虑分解一个像 78893012641 这样的数字,其素数分解为 280879^2。您将搜索最多 280879 的每个素数:昂贵,但几乎是不可避免的。但是,此时您除以 280879,然后尝试分解 280879,从 2 开始并再次扫描所有小素数,即使您刚刚发现它们都不是因式!

正如夏立耀在评论中所说,我也会怀疑在取模之前对非常大的Integers 进行乘法,而不是在每次乘法后取模。

【讨论】:

  • factorization 函数在输入是一个大素数时更糟糕,它会尝试所有素数,直到达到那个素数。并且primes 列表是共享的,因此即使对于小于 2^64 的素数,它也会占用你所有的内存。
  • 我完全忽略了那部分!我本可以让它更好地平方根。谢谢你的回答。
  • n 的问题限制为 10^6。 (OP没有在帖子中包含这个限制,它只出现在链接页面上)。正确的分解算法只需要最多 1000 个素数。在这里使用最佳试验筛代替埃拉托色尼筛是完全可以的。
【解决方案2】:

您没有从“互联网”复制正确的代码。您应该为素数列表复制 primesTMWE,但更重要的是,为因式分解算法复制 primeFactors

您基于foldl 计算一个数的因式分解的除数非常好,但也许应该使用foldl'

请注意solve nsolve (n-1) 都计算fact (n-1),因此更好地预先计​​算所有它们......也许存在更好的算法来找到从1 到@987654334 的所有数字的除数@而不是分别为每个数字计算它。

我怀疑即使使用正确的算法(我在上面链接),如果您要独立分解每个数字(O(n) 个数字, O(n1/2)) 时间来分解每个...每个素数,至少)。

也许这里要尝试的是 最小因子筛,它可以在 O(n log log n)中构建> 像往常一样使用 Eratosthenes 的筛子,一旦构建完成,它可以让您在 O(log log n) 时间内找到每个数字的因式分解(这是一个数字的average number of prime factors)。不过,它必须建立到 n (当然,您可以特殊情况下将空间需求减半;或者 6-coprimes 以节省另一个 1 /6)。可能是as an STUArray(该链接是一个示例;可以找到更好的代码here on SO)。

最小因子筛与埃拉托色尼筛一样,只是它使用最小因子而不是布尔值作为标记。

要找到一个数字的因式分解,我们只需重复删除一个数字的最小因子 n / sf(n) =: n1,重复删除 n1 / sf(n1) =: n2,然后是 n2,等等,直到我们遇到一个素数(这是任何具有 本身作为最小因素)。

由于您只使用这些因素来计算数字的除数总数,因此您可以将这两个计算融合到一个连接的循环中,以提高效率。

【讨论】:

  • 分解部分和我今天早上尝试的类似。我的只是通过递归减少最小的因子来计算因子的数量。但它对时间的改善不大。我现在正在查看其他人使用 Haskell 的解决方案,他们甚至没有建立一个主要列表。我怀疑我在第一步就错了。
  • 您可以使用最新代码发布新问题。
  • 您的评论将两个问题混为一谈。一个是找到分解,另一个是计算不同除数的数量。您已经拥有函数fact,它在 log log n 时间执行第二部分。为了有效地做第一个,你需要建立最小的因子筛(需要 N log log N 时间),然后用它来找到每个数字 n 的因式分解它,在 log log n 时间。这比 n^(1/2)n^(1/3) 要好得多。
猜你喜欢
  • 1970-01-01
  • 2017-07-10
  • 2021-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-19
  • 2018-08-23
  • 2020-08-06
相关资源
最近更新 更多