【问题标题】:How can I optimize this code in Haskell如何在 Haskell 中优化此代码
【发布时间】:2014-06-04 21:10:58
【问题描述】:

关于优化,我需要您的帮助。问题来了:

结合常用算术运算符,用数字 3 的 10 倍无法得到的最小(严格正数)自然数是多少?

这是我的解决方案:

import Ratio

num x 1 = [x]
num x n = num'' $ concat $ [ num' a b | i <- [1..n`quot`2], a <- num x i, b <- num x (n-i) ]
  where  num' a b | a==0      = [b] 
                  | b==0      = [a]
                  | otherwise = [a+b,abs(a-b),a*b,a/b,b/a]
         num'' (x:r) = x : num'' (filter (x/=) r)
         num'' _      = []

cint x = map numerator . filter ((1==) . denominator) . num x

firstNumber x n = take 1 [ i | i <- [1..], i `notElem` (cint x n) ]

但是效率太低了。例如,当我调用 firstNumber 3 7 时,需要将近 30 秒才能看到结果。但是,firstNumber 3 10的结果我等了好久都没有看到。我该如何优化它?

【问题讨论】:

  • 你编译过优化吗?另外,你能再解释一下这个问题吗? “使用 3 的 10 次无法获得的第一个数字”是什么意思?具体来说,我不明白“10 乘以 3”是什么意思。
  • 一个提示:你有num''(不是一个很好的函数名IMO),它基本上可以确保你的列表中没有重复。为什么不使用Data.Set?这是对唯一元素的无序集合的更有效实现,因为在内部它是作为二叉树实现的。您可能仅使用num'' = Data.Set.toList . Data.Set.fromList 就可以节省大量时间,除非num'' 旨在接受无限流。由于看起来您的搜索空间有限,因此应该是安全的。
  • 您将使用 3,3,3,3,3,3,3,3,3,3(3 的 10 次)。以及每个 3 之间的四个操作数(+、-、*、/)。考虑所有可能性。例如,当你调用 firstNumber 3 6 时,你得到 1,2,3,...31 但不是 32。现在明白了吗?
  • 好的,我正在尝试。谢谢。
  • 您不是在寻找最小的数字吗?那么得到正确的“第一个”的顺序是什么?除了您的列表理解定义遵循该顺序。那么,你的目标不是什么?

标签: haskell optimization


【解决方案1】:

我们可以通过生成部分答案集来解决这个问题:num k 1 是集合 {k},num 2 是通过组合 num k 1 和 num k 1 生成的所有数字的集合,num k 3是由num k 1 与num k 2 组合生成的所有数字的集合,依此类推。每个步骤都使用前面步骤中计算的集合,并应用一个运算符。这是前 3 个步骤。请注意,每个数字都是使用两个先前生成的数字和一个运算符计算得出的。

  1. num 3 1 = {3}
  2. num 3 2 = {3-3=0, 3/3=1, 3=3, 3+3=6, 3*3=9}
  3. num 3 3 = {3-9=-6, 3-6=-3, 1-3=-2, 0=0, 1/3=1/3, 3/6=1/2, 1=1, 6/3=2, 3=3, 3+1=4, 6=6, 9=9, 3+9=12, 3*6=18, 3*9=27}

基于列表的num 函数正在重新计算之前的步骤,原因有两个。

  • Step n 从头开始​​重新计算之前的所有步骤。例如,num x 4 将计算 num x 1、num x 2 和 num x 3。然后,num x 3 将再次计算 num x 1 和 num x 2。
  • 在内部循环中有一个对num 的递归调用。具体来说,您有[... | ... a &lt;- num x i, b &lt;- num x (n-i) ]。这将为a 的每个值重新计算num x (n-i)。您可以通过编写[... | ... let b_input = num x (n-i), a &lt;- num x i, b &lt;- b_input] 将递归调用移出内部循环。 (编译器优化可能会自动执行此操作,但您不应依赖它。)

您应该保存并重复使用它们,而不是重新计算以前的结果。最简单的方法是在算法进行时保存先前结果的列表。转换代码以保存以前的结果是称为 memoization 的更通用技术的一个实例。

效率低下的另一个原因是num'',它搜索整个列表以在二次时间中删除重复项。可以使用 Data.Set 模块中的集合在 n*log(n) 时间内删除重复项。

总之,在num k n,不要递归调用num,因为这会做多余的工作。不要递归调用,而是保存来自num k 1、num k 2、...num k (n-1) 的结果列表并将此列表传递给num k n。另外,使用Data.Set 模块删除重复值,而不是调用num''。

【讨论】:

  • 我将如何使用 Data.Set 模块来删除重复值而不是调用 num'' ?可以举个例子吗?
  • @sasas 你可以在stackoverflow.com/q/16108714/507803的答案中看到一些示例实现。
  • 很遗憾,我没能成功。
  • @sasas 如果您需要更详细的 Haskell 或算法指导,您可以在 freenode IRC 频道上提问。我认为 IRC 是一种更好的教学媒介。
猜你喜欢
  • 2011-02-24
  • 2011-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-02
  • 1970-01-01
  • 2017-06-13
相关资源
最近更新 更多