Haskell中动态编程的高效表

时间:2011-03-07 18:18:56

标签: haskell lazy-evaluation dynamic-programming memoization knapsack-problem

我在Haskell中编写了0-1 Knapsack problem。到目前为止,我对于懒惰和普遍性水平感到非常自豪。

我首先提供创建和处理惰性2d矩阵的函数。

mkList f = map f [0..]
mkTable f = mkList (\i -> mkList (\j -> f i j))

tableIndex table i j = table !! i !! j

然后我为给定的背包问题制作一个特定的表

knapsackTable = mkTable f
    where f 0 _ = 0
          f _ 0 = 0
          f i j | ws!!i > j = leaveI
                | otherwise = max takeI leaveI
              where takeI  = tableIndex knapsackTable (i-1) (j-(ws!!i)) + vs!!i
                    leaveI = tableIndex knapsackTable (i-1) j

-- weight value pairs; item i has weight ws!!i and value vs!!i
ws  = [0,1,2, 5, 6, 7] -- weights
vs  = [0,1,7,11,21,31] -- values

最后使用几个帮助函数来查看表格

viewTable table maxI maxJ = take (maxI+1) . map (take (maxJ+1)) $ table
printTable table maxI maxJ = mapM_ print $ viewTable table maxI maxJ

这很容易。但我想更进一步。

我想要一个更好的数据结构。理想情况下,它应该是

  • 未装箱(不可变) [编辑]别介意
  • 懒惰
  • 无界
  • O(1)时间构建
  • O(1)查找给定条目的时间复杂度,
    (更现实地,最坏的是O(log n),其中n是i*j,用于查找第i行第j列的条目

如果你能解释为什么/你的解决方案如何满足这些理想,那么可以获得奖励。

如果您可以进一步概括knapsackTable并证明它是有效的,也可以获得奖励积分。

在改进数据结构时,您应该尝试满足以下目标:

  • 如果我要求最大权重为10的解决方案(在我当前的代码中,那将是indexTable knapsackTable 5 10,5意味着包括项目1-5),只需要执行最少量的工作。理想情况下,这意味着没有O(i*j)用于强制表的每一行的主干到必要的列长度。你可以说这不是“真正的”DP,如果你认为DP意味着评估整个表格。
  • 如果我要求打印整个表(类似printTable knapsackTable 5 10),则每个条目的值应该只计算一次。给定单元格的值应该取决于其他单元格的值(DP样式:这个想法是,永远不会重新计算相同的子问题两次)

思路:

对我所陈述的理想做出某些妥协的答案 将被赞成(反正由我),只要它们提供信息。答案最少的答案可能是“接受”的答案。

5 个答案:

答案 0 :(得分:14)

首先,您对未装箱数据结构的标准可能有点误导。未装箱的值必须严格,并且它们与不变性无关。我要提出的解决方案是不可变,懒惰和盒装。另外,我不确定你想以什么方式构建和查询是O(1)。我提议的结构是懒洋洋地构造的,但由于它可能是无限的,它的完整构造将花费无限的时间。查询结构将花费大约k的任何特定键的O(k)时间,但当然,您正在查找的值可能需要更长的时间来计算。

数据结构是一个懒惰的特里。我在我的代码中使用了Conal Elliott的MemoTrie库。对于通用性,它采用函数而不是权重和值的列表。

knapsack :: (Enum a, Num w, Num v, Num a, Ord w, Ord v, HasTrie a, HasTrie w) =>
            (a -> w) -> (a -> v) -> a -> w -> v
knapsack weight value = knapsackMem
  where knapsackMem = memo2 knapsack'
        knapsack' 0 w = 0
        knapsack' i 0 = 0
        knapsack' i w
          | weight i > w = knapsackMem (pred i) w
          | otherwise = max (knapsackMem (pred i) w)
                        (knapsackMem (pred i) (w - weight i)) + value i

基本上,它是作为一个具有懒惰脊柱和惰性值的trie实现的。它只受键类型的限制。因为整个事情是懒惰的,所以在用查询强制它之前它的构造是O(1)。每个查询都强制沿trie及其值的单个路径,因此对于有界密钥大小 O(log n),它是 O(1)。正如我已经说过的那样,它是不可改变的,但并非拆箱。

它将共享递归调用中的所有工作。它实际上不允许你直接打印trie,但是这样的事情不应该做任何多余的工作:

mapM_ (print . uncurry (knapsack ws vs)) $ range ((0,0), (i,w))

答案 1 :(得分:9)

Unboxed意味着严格和有界。 100%未装箱的任何东西都不能是懒惰或无限制的。通常的妥协体现在将[Word8]转换为Data.ByteString.Lazy,其中有未装箱的块(严格的ByteString),它们以无限的方式懒洋洋地连接在一起。

可以使用“scanl”,“zipWith”和我的“takeOnto”来制作更高效的表生成器(增强以跟踪单个项目)。这有效地避免在创建表时使用(!!):

import Data.List(sort,genericTake)

type Table = [ [ Entry ] ]

data Entry = Entry { bestValue :: !Integer, pieces :: [[WV]] }
  deriving (Read,Show)

data WV = WV { weight, value :: !Integer }
  deriving (Read,Show,Eq,Ord)

instance Eq Entry where
  (==) a b = (==) (bestValue a) (bestValue b)

instance Ord Entry where
  compare a b = compare (bestValue a) (bestValue b)

solutions :: Entry -> Int
solutions = length . filter (not . null) . pieces

addItem :: Entry -> WV -> Entry
addItem e wv = Entry { bestValue = bestValue e + value wv, pieces = map (wv:) (pieces e) }

-- Utility function for improve
takeOnto :: ([a] -> [a]) -> Integer -> [a] -> [a]
takeOnto endF = go where
  go n rest | n <=0 = endF rest
            | otherwise = case rest of
                            (x:xs) -> x : go (pred n) xs
                            [] -> error "takeOnto: unexpected []"

improve oldList wv@(WV {weight=wi,value = vi}) = newList where
  newList | vi <=0 = oldList
          | otherwise = takeOnto (zipWith maxAB oldList) wi oldList
  -- Dual traversal of index (w-wi) and index w makes this a zipWith
  maxAB e2 e1 = let e2v = addItem e2 wv
                in case compare e1 e2v of
                     LT -> e2v
                     EQ -> Entry { bestValue = bestValue e1
                                 , pieces = pieces e1 ++ pieces e2v }
                     GT -> e1

-- Note that the returned table is finite
-- The dependence on only the previous row makes this a "scanl" operation
makeTable :: [Int] -> [Int] -> Table
makeTable ws vs =
  let wvs = zipWith WV (map toInteger ws) (map toInteger vs)
      nil = repeat (Entry { bestValue = 0, pieces = [[]] })
      totW = sum (map weight wvs)
  in map (genericTake (succ totW)) $ scanl improve nil wvs

-- Create specific table, note that weights (1+7) equal weight 8
ws, vs :: [Int]
ws  = [2,3, 5, 5, 6, 7] -- weights
vs  = [1,7,8,11,21,31] -- values

t = makeTable ws vs

-- Investigate table

seeTable = mapM_ seeBestValue t
  where seeBestValue row = mapM_ (\v -> putStr (' ':(show (bestValue v)))) row >> putChar '\n'

ways = mapM_ seeWays t
  where seeWays row = mapM_ (\v -> putStr (' ':(show (solutions v)))) row >> putChar '\n'

-- This has two ways of satisfying a bestValue of 8 for 3 items up to total weight 5
interesting = print (t !! 3 !! 5) 

答案 2 :(得分:4)

懒惰的可存储载体:http://hackage.haskell.org/package/storablevector

无限制,懒惰,O(chunksize)时间构造,O(n / chunksize)索引,其中chunksize可以足够大以用于任何给定目的。基本上是一个懒惰的列表,其中包含一些显着的常数因素。

答案 3 :(得分:4)

为了记忆功能,我推荐像Luke Palmer的memo combinators这样的库。该库使用try,它是无限的并且具有O(密钥大小)查找。 (一般情况下,你不能比O(密钥大小)查找更好,因为你总是需要触摸密钥的每一位。)

knapsack :: (Int,Int) -> Solution
knapsack = memo f
    where
    memo    = pair integral integral
    f (i,j) = ... knapsack (i-b,j) ...


在内部,integral组合器可能构建无限数据结构

data IntTrie a = Branch IntTrie a IntTrie

integral f = \n -> lookup n table
     where
     table = Branch (\n -> f (2*n)) (f 0) (\n -> f (2*n+1))

Lookup的工作原理如下:

lookup 0 (Branch l a r) = a
lookup n (Branch l a r) = if even n then lookup n2 l else lookup n2 r
     where n2 = n `div` 2

还有其他方法可以构建无限尝试,但这种方法很受欢迎。

答案 4 :(得分:2)

为什么不使用Data.Map将其他Data.Map放入其中?据我所知,它很快。 但它不会很懒惰。

不仅如此,您还可以为数据实现Ord类型类

data Index = Index Int Int

并直接将二维索引作为键。您可以通过将此地图生成为列表来实现懒惰,然后使用

fromList [(Index 0 0, value11), (Index 0 1, value12), ...]