Haskell以3n + 1挑战的方式

时间:2016-02-16 20:42:19

标签: haskell functional-programming

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

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

我一直在寻找一种Haskell方法来解决比Java或C ++更具竞争性的问题(以便符合允许的运行时限制)。虽然一个简单的Java解决方案可以记住任何已经计算过的循环的循环长度。我没有成功地应用这个想法来获得Haskell解决方案。

我已经尝试了Data.Function.Memoize,以及使用此帖子中的想法的自制日志时间记忆技术:Memoization in Haskell?。不幸的是,memoization实际上使循环(n)的计算更慢。我相信减速来自于haskell方式的开销。 (我尝试使用已编译的二进制代码运行,而不是解释。)

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

你能帮忙告诉一个整洁的竞争Haskell计划吗?

谢谢!

2 个答案:

答案 0 :(得分:4)

>>> 使用下面的方法,我可以submit an accepted answer to SPOJ。您可以查看here中的完整代码。

问题已界定0 < n < 1,000,000。预先计算所有并将它们存储在数组中;然后冻结数组。该数组可用作自己的缓存/ memoization空间

然后问题将减少到数组上的范围查询问题,这可以使用树非常有效地完成。

使用下面的代码,我可以在几分之一秒内得到1..1,000,000的Collat​​z:

$ time echo 1000000 | ./collatz 
525

real    0m0.177s
user    0m0.173s
sys     0m0.003s

注意下面的collatz函数,在内部使用可变STUArray,但它本身是一个纯函数:

import Control.Monad.ST (ST)
import Control.Monad (mapM_)
import Control.Applicative ((<$>))
import Data.Array.Unboxed (UArray, elems)
import Data.Array.ST (STUArray, readArray, writeArray, runSTUArray, newArray)

collatz :: Int -> UArray Int Int
collatz size = out
    where
    next i = if odd i then 3 * i + 1 else i `div` 2

    loop :: STUArray s Int Int -> Int -> ST s Int
    loop arr k
        | size < k  = succ <$> loop arr (next k)
        | otherwise = do
            out <- readArray arr k
            if out /= 0 then return out
            else do
                out <- succ <$> loop arr (next k)
                writeArray arr k out
                return out

    out = runSTUArray $ do
        arr <- newArray (1, size) 0
        writeArray arr 1 1
        mapM_ (loop arr) [2..size]
        return arr

main = do
    size <- read <$> getLine
    print . maximum . elems $ collatz size

为了对此数组执行范围查询,您可以构建一个平衡树,如下所示:

type Range = (Int, Int)
data Tree  = Leaf Int | Node Tree Tree Range Int

build_tree :: Int -> Tree
build_tree size = loop 1 cnt
    where
    ctz = collatz size
    cnt = head . dropWhile (< size) $ iterate (*2) 1

    (Leaf a)       +: (Leaf b)       = max a b
    (Node _ _ _ a) +: (Node _ _ _ b) = max a b

    loop lo hi
        | lo == hi  = Leaf $ if size < lo then minBound else ctz ! lo
        | otherwise = Node left right (lo, hi) (left +: right)
        where
        i = (lo + hi) `div` 2
        left  = loop lo i
        right = loop (i + 1) hi

query_tree :: Tree -> Int -> Int -> Int
query_tree (Leaf x) _ _ = x
query_tree (Node l r (lo, hi) x) i j
    | i <= lo && hi <= j = x
    | mid < i       = query_tree r i j
    | j   < 1 + mid = query_tree l i j
    | otherwise     = max (query_tree l i j) (query_tree r i j)
    where mid = (lo + hi) `div` 2

答案 1 :(得分:2)

这与其他答案中的相同,但是使用了一个不可变的递归定义数组(并且它也有轻微泄漏(有人会说为什么?)并且因此慢了两倍):

import Data.Array

upper = 10^6

step :: Integer -> Int
step i = 1 + colAt (if odd i then 3 * i + 1 else i `div` 2)

colAt :: Integer -> Int
colAt i | i > upper = step i
colAt i = col!i

col :: Array Integer Int
col = array (1, upper) $ (1, 1) : [(i, step i) | i <- [2..upper]]

main = print $ maximum $ elems col