计算所有可能游戏的“最小数量获胜”游戏结果 - 内存不足

时间:2012-03-12 19:50:23

标签: haskell

我正在学习在haskell中思考和编码。 “最小数量获胜”的游戏:n人在1和n之间的数字上下注,而只有一次下注的最小数字获胜。

我正在计算n = 10的所有可能的投注系数并计算获胜者数字。是的,这段代码并不能完全解决这个问题,但这不是我的观点,而是我的代码,内存耗尽相对较快。

(添加评论 - 抱歉!)

import Data.Array
import Data.List

f xs = flip map [1..10] $ flip (:) xs
p 1 = f []
p n =  concat $ map f $ p (n-1)
--the above, (p n) generates the list of all possible [a1, a2, ..., an] lists, where ai=1..10
--p 2 = [[1,1],[2,1],[3,1],[4,1],[5,1],...,[10,10]

--my first shot at the countidens function, the functionality stays the same with the other
--countidens2 xs = map (\x->(head x, length x)) $ group $ sort xs

countidens' xs = accumArray (+) 0 (1,10) $ zip xs $ repeat 1
countidens xs = filter ((/=) 0 . snd) $ zip [1..10] $ map ((countidens' xs)!) [1..10]
--counts the number of occurrences of each number (1..10) in a list
--countidens [1,1,1,2,2,3] = (1,3),(2,2),(3,1)]
--(the above, countidens2 is much easier to understand)

numlist n = map (flip (++) ([(0,0)])) $ map countidens $ p n
--maps countidens on the (p n) list, and attaches a dummy (0,0) to the end (this is needed later)

g (x, (y, z)) | (x==y) && (z==1)    = True
              | (x < y)             = True
              | (y==0)              = True
              | otherwise           = False
-- filter function for [(a, (a,a)] lists - (a1, (a1, a)) -> Bool

winners n = map fst $ map (head . filter g) $ map (zip [1..]) $ numlist n
-- extracts the number of the first element of (numlist n) that qualifies as g
--    for each element of g (note: these are results of the countidens function, since that was mapped)
-- the dummy (0,0) was needed so there's always one that does

winnernumsarr n = accumArray (+) 0 (1,10) $ flip zip (repeat 1) $ winners n
-- winners n produces a simple list of integers (1..10) that is 10^n long, this (winnernumsarr) accumulates the number of each integer, much like countidens did
-- (but does not produce a fancy output)

main = putStrLn $ show $ winnernumsarr 7 -- aiming for 10! even 8 runs out of memory on my machine

虽然我知道这段代码不能完全按照我的意愿去做,但更重要的是,这不是我第一次遇到haskell的“内存不足”问题,而且我遇到了问题know可以用C ++编写,只使用少量内存。

一定有办法 - 但是如何?

3 个答案:

答案 0 :(得分:1)

这里有两件事很重要。输入签名和未装箱的数组。

module Main (main) where

import Data.Array.Unboxed
import Data.List

f xs = flip map [1..10] $ flip (:) xs
p 1 = f []
p n =  concat $ map f $ p (n-1)

--my first shot at the countidens function, the functionality stays the same with the other
--countidens2 xs = map (\x->(head x, length x)) $ group $ sort xs

countidens' :: [Int] -> UArray Int Int
countidens' xs = accumArray (+) 0 (1,10) $ zip xs $ repeat 1

countidens xs = filter ((/=) 0 . snd) $ assocs (countidens' xs)

numlist n = map (flip (++) ([(0,0)])) $ map countidens $ p n

g (x, (y, z)) | (x==y) && (z==1)    = True
              | (x < y)             = True
              | (y==0)              = True
              | otherwise           = False

winners n = map fst $ map (head . filter g) $ map (zip [1..]) $ numlist n

winnernumsarr :: Int -> UArray Int Int
winnernumsarr n = accumArray (+) 0 (1,10) $ flip zip (repeat 1) $ winners n
main = putStrLn $ show $ winnernumsarr 7

在很小的空间内运行,虽然速度很慢(8秒需要50秒,7秒需要4.9秒)。

当您使用盒装数组时,accumArray不会将纯数字写入数组,而是使用thunk。在winnernumsarr中,thunk变得巨大。这需要大量内存,并且最终需要大量的堆栈空间来进行评估。使用未装箱的阵列,添加会在它们到来时执行,而不是构建巨大的阵容。

类型签名是修复要打印的数组类型以及使所有出现的数字类型Int以减少分配和提高速度所必需的。

在不改变算法的情况下,更加惯用的版本是

module Main (main) where

import Data.Array.Unboxed
import Data.List

p :: Int -> [[Int]]
p 0 = [[]]
p n = [k:xs | xs <- p (n-1), k <- [1 .. 10]]

countidens' :: [Int] -> UArray Int Int
countidens' xs = accumArray (+) 0 (1,10) $ map (\k -> (k,1)) xs

countidens :: [Int] -> [(Int,Int)]
countidens = filter ((/=) 0 . snd) . assocs . countidens'

numlist n = map ((++[(0,0)]) . countidens) $ p n

g :: (Int,(Int,Int)) -> Bool
g (x, (y, z)) | (x==y) && (z==1)    = True
              | (x < y)             = True
              | (y==0)              = True
              | otherwise           = False

winners :: Int -> [Int]
winners n = map fst $ map (head . filter g) $ map (zip [1..]) $ numlist n

winnernumsarr :: Int -> UArray Int Int
winnernumsarr n = accumArray (+) 0 (1,10) $ map (\k -> (k,1)) $ winners n

main :: IO ()
main = print $ winnernumsarr 7

这也更快。一点点加速来自GHC可以更好地优化列表生成函数p的这种形式,大部分来自用zip xs (repeat 1)替换map (\k -> (k,1)) xs。我必须承认,我不明白为什么会产生如此大的影响,但zip必须将两个列表与_ : _匹配,而map只需匹配xs ,这节省了一些工作。

答案 1 :(得分:0)

我很难理解你的代码究竟在做什么,所以我只是编写了一个函数bets,它接受​​了所有可能的赌注的懒惰列表中的玩家数量和吐出。

-- `bets n` calculates all possible sequences of bets with `n` players.
-- It returns a list of lists, each sub-list being `n` in length
bets :: Int -> [[Int]]
bets n = bets' n
  where bets' :: Int -> [[Int]] -- use separate function so we always have the total `n` available
        bets' n'
          | n' == 0 = [[]]
          | n' > 0  = concatMap step $ bets' (pred n')
          | otherwise = error "bets: negative number of players"
        step :: [Int] -> [[Int]]
        step bs = zipWith (:) [1..n] (repeat bs)

我用n == 5进行了测试,效果非常好。我不知道你期望用n == 10表现出什么样的表现,所以这有可能最终对你来说太慢了。

答案 2 :(得分:0)

我建议您从数组(甚至是未装箱的数组)切换到Vector库。界面更加丰富,基于融合的矢量实现通常可以带来性能优势。

这是使用Vector的等效版本,其中包含了一些Daniel Fisher的更改:

{-# LANGUAGE TupleSections #-}

import qualified Data.Vector.Unboxed as V
import Data.List

p :: Int -> [[Int]]
p 0 = [[]]
p n = [k:xs | xs <- p (n-1), k <- [1 .. 10]]

countidens' :: [Int] -> V.Vector Int
countidens' xs = V.accum (+) (V.replicate 11 0) $ map (,1) xs

countidens = V.filter ((/= 0) . snd) . V.indexed . countidens'

numlist = map ((`V.snoc` (0,0)) . countidens) . p

g (x, (y, z)) | (x==y) && (z==1)    = True
              | (x < y)             = True
              | (y==0)              = True
              | otherwise           = False

winners n = map (fst . V.head . V.filter g . V.imap (\ix a -> (ix+1,a)) ) $ numlist n
winnernumsarr :: Int -> V.Vector (Int,Int)
winnernumsarr n = V.tail . V.indexed $ V.accum (+) (V.replicate 11 0)
  $ flip zip (repeat 1) $ winners n
main = putStrLn $ show $ winnernumsarr 8

在我的系统上,这会将运行时间从49秒减少到31秒,两个程序都使用“-O2 -msse2”进行编译。

两个警告:首先,Vector实现向量,因此如果您需要多维索引,您可能希望继续使用数组。其次,向量是0索引的,因此您可能需要对代码的其余部分进行适当的调整。