Haskell解决3n + 1问题的方法


12

这是来自SPOJ的一个简单的编程问题:http : //www.spoj.com/problems/PROBTRES/

基本上,要求您为i和j之间的数字输出最大的Collat​​z周期。(数字$ n $的Colatz周期是最终从$ n $变为1的步骤数。)

我一直在寻找一种Haskell方法,以比Java或C ++更好的性能来解决问题(以适应允许的运行时限制)。尽管可以记住任何已计算周期的周期长度的简单Java解决方案将起作用,但是我并未成功地将其应用于获得Haskell解决方案的想法。

我已经尝试了Data.Function.Memoize,以及使用这篇文章中的想法的自制日志时间备注技术:https ://stackoverflow.com/questions/3208258/memoization-in-haskell 。不幸的是,记忆实际上使cycle(n)的计算更加缓慢。我认为,放缓的原因来自Haskell方式的开销。(我尝试使用编译的二进制代码运行,而不是进行解释。)

我还怀疑简单地将数字从i迭代到j可能会很昂贵($ i,j \ le10 ^ 6 $)。因此,我甚至尝试使用http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html中的想法为范围查询预先计算所有内容。但是,这仍然会出现“超出时间限制”错误。

您能为此告知一个精巧的竞争性Haskell程序吗?


10
这篇帖子对我来说似乎不错。这是一个算法问题,需要适当的设计才能实现适当的性能。我们在这里真正不想要的是“我该如何修复损坏的代码”问题。
罗伯特·哈维

Answers:


7

我将在Scala中回答,因为我的Haskell不那么新鲜,因此人们会相信这是一个通用的函数编程算法问题。我将坚持易于转移的数据结构和概念。

我们可以从生成collat​​z序列的函数开始,该函数相对简单,除了需要将结果作为参数传递以使其尾部递归:

def collatz(n: Int, result: List[Int] = List()): List[Int] = {
   if (n == 1) {
     1 :: result
   } else if ((n & 1) == 1) {
     collatz(3 * n + 1, n :: result)
   } else {
     collatz(n / 2, n :: result)
   }
 }

这实际上将序列置于相反的顺序,但这对于下一步(将长度存储在地图中)非常理想:

def calculateLengths(sequence: List[Int], length: Int,
  lengths: Map[Int, Int]): Map[Int, Int] = sequence match {
    case Nil     => lengths
    case x :: xs => calculateLengths(xs, length + 1, lengths + ((x, length)))
}

您可以使用第一步的答案,初始长度和一个空的映射(例如)来调用它calculateLengths(collatz(22), 1, Map.empty))。这就是您记住结果的方式。现在我们需要进行修改collatz才能使用此功能:

def collatz(n: Int, lengths: Map[Int, Int], result: List[Int] = List()): (List[Int], Int) = {
  if (lengths contains n) {
     (result, lengths(n))
  } else if ((n & 1) == 1) {
    collatz(3 * n + 1, lengths, n :: result)
  } else {
    collatz(n / 2, lengths, n :: result)
  }
}

我们省去了n == 1检查,因为我们可以用初始化地图1 -> 1,但是我们需要增加1放入地图内部的长度calculateLengths。现在,它还会返回停止递归的记忆长度,我们可以使用它来初始化calculateLengths,例如:

val initialMap = Map(1 -> 1)
val (result, length) = collatz(22, initialMap)
val newMap = calculateLengths(result, lengths, initialMap)

现在我们已经有了相对高效的实现,我们需要找到一种方法将前一个计算的结果输入到下一个计算的输入中。这称为fold,看起来像:

def iteration(lengths: Map[Int, Int], n: Int): Map[Int, Int] = {
  val (result, length) = collatz(n, lengths)
  calculateLengths(result, length, lengths)
}

val lengths = (1 to 10).foldLeft(Map(1 -> 1))(iteration)

现在要找到实际的答案,我们只需要过滤给定范围之间的映射中的键,并找到最大值即可得到最终结果:

def answer(start: Int, finish: Int): Int = {
  val lengths = (start to finish).foldLeft(Map(1 -> 1))(iteration)
  lengths.filterKeys(x => x >= start && x <= finish).values.max
}

在我的REPL中,范围为1000左右,就像示例输入一样,答案几乎立即返回。


3

Karl Bielefeld已经很好地回答了这个问题,我只添加一个Haskell版本。

首先是基本算法的一个简单的,非记忆性的版本,以展示有效的递归:

simpleCollatz :: Int -> Int -> Int
simpleCollatz count 1 = count + 1
simpleCollatz count n | odd n     = simpleCollatz (count + 1) (3 * n + 1)
                      | otherwise = simpleCollatz (count + 1) (n `div` 2)

那几乎是不言自明的。

我也将使用简单的方法Map来存储结果。

-- double imports to make the namespace pretty
import           Data.Map  ( Map )
import qualified Data.Map as Map

-- a new name for the memoizer
type Store = Map Int Int

我们始终可以在商店中查找最终结果,因此对于单个值,签名为

memoCollatz :: Int -> Store -> Store

让我们从结束案例开始

memoCollatz 1 store = Map.insert 1 1 store

是的,我们可以事先添加,但我不在乎。请下一个简单的案例。

memoCollatz n store | Just _ <- Map.lookup n store = store

如果存在该值,则为。仍然无所作为。

                    | odd n     = processNext store (3 * n + 1)
                    | otherwise = processNext store (n `div` 2)

如果没有价值,我们必须做点什么。让我们将其放入本地函数中。请注意,这部分看起来非常接近“简单”解决方案,只是递归稍微复杂一点。

  where processNext store'' next | Just count <- Map.lookup next store''
                                 = Map.insert n (count + 1) store''

现在我们终于可以做点什么了。如果我们在store''(边注:有两个haskell语法突出显示器,但是一个很丑,另一个被质数符号弄糊涂。这是双素数的唯一原因。),我们发现了计算的值,我们只需添加新的值。但是现在变得有趣了。如果找不到该值,则必须计算它并进行更新。但是我们已经具有两者的功能!所以

                                | otherwise
                                = processNext (memoCollatz next store'') next

现在,我们可以有效地计算单个值。如果我们要计算几个,我们只是通过折叠传递。

collatzRange :: Int -> Int -> Store
collatzRange lower higher = foldr memoCollatz Map.empty [lower..higher]

(在这里,您可以初始化1/1大小写。)

现在,我们要做的就是提取最大值。目前,商店中的值不能大于该范围内的值,因此可以说

collatzRangeMax :: Int -> Int -> Int
collatzRangeMax lower higher = maximum $ collatzRange lower higher

当然,如果您要计算多个范围并在这些计算之间共享存储(折叠是您的朋友),则需要一个过滤器,但这不是这里的主要重点。


1
为了提高速度,Data.IntMap.Strict应该使用。
奥拉西
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.