【问题标题】:How is this memoized DP table too slow for SPOJ?这个记忆的 DP 表对 SPOJ 来说太慢了吗?
【发布时间】:2011-11-22 11:32:37
【问题描述】:

剧透:我正在处理http://www.spoj.pl/problems/KNAPSACK/,所以如果您不想为您破坏可能的解决方案,请不要偷看。

样板:

import Data.Sequence (index, fromList)
import Data.MemoCombinators (memo2, integral)

main = interact knapsackStr

knapsackStr :: String -> String
knapsackStr str = show $ knapsack items capacity numItems
  where [capacity, numItems] = map read . words $ head ls
        ls = lines str
        items = map (makeItem . words) $ take numItems $ tail ls

设置舞台的一些类型和助手:

type Item = (Weight, Value)
type Weight = Int
type Value = Int

weight :: Item -> Weight
weight = fst

value :: Item -> Value
value = snd

makeItem :: [String] -> Item
makeItem [w, v] = (read w, read v)

以及主要功能:

knapsack :: [Item] -> Weight -> Int -> Value
knapsack itemsList = go
  where go = memo2 integral integral knapsack'
        items = fromList $ (0,0):itemsList
        knapsack' 0 _ = 0
        knapsack' _ 0 = 0
        knapsack' w i | wi > w    = exclude
                      | otherwise = max exclude include
          where wi = weight item
                vi = value item
                item = items `index` i
                exclude = go w (i-1)
                include = go (w-wi) (i-1) + vi

这段代码有效;我尝试插入 SPOJ 示例测试用例,它会产生正确的结果。但是当我将这个解决方案提交给 SPOJ(而不是导入 Luke Palmer 的 MemoCombinators,我只是将必要的部分复制并粘贴到提交的源中)时,它超过了时间限制。 =/

我不明白为什么;我asked earlier 介绍了一种执行 0-1 背包的有效方法,并且我相当确信这几乎是最快的:一个记忆函数,它只会递归地计算它绝对需要的子条目,以便产生正确的结果。我是否以某种方式搞砸了记忆?这段代码中是否有我遗漏的慢点? SPOJ 只是对 Haskell 有偏见吗?

我什至把{-# OPTIONS_GHC -O2 #-} 放在了提交的顶部,但是很可惜,它没有帮助。我尝试了一个类似的解决方案,它使用Sequences 的二维数组,但它也因为太慢而被拒绝。

【问题讨论】:

  • 您尝试过分析吗?总的来说,我还没有发现记忆对大多数任务有那么大的帮助。此外,在这里使用序列似乎没有多大帮助,也许是 Map
  • 您能否向我们展示完整的代码以及提供的必要内存位?这样可以更轻松地诊断问题。
  • index 操作是O(log(min(i,n-i)))(根据黑线鳕)。也许你应该使用数组或向量。
  • @DanBurton:当然没有,但没有它,您需要所有这些 thunk 或巨大的 dp 数组。还有时间 - mineyours 在最大尺寸的自定义输入上(重写了 haskell 中已有的内容)。

标签: performance haskell memoization knapsack-problem


【解决方案1】:

有一个主要问题确实减慢了这一速度。它太多态了。函数的类型专用版本可以比多态变种快得多,并且无论出于何种原因,GHC 都没有将此代码内联到可以确定使用的确切类型的程度。当我将integral 的定义更改为:

integral :: Memo Int
integral = wrap id id bits

我得到了大约 5 倍的加速;我认为它的速度足以被 SPOJ 接受。

然而,这仍然比 gorlum0 的解决方案慢得多。我怀疑原因是因为他使用的是数组,而您使用的是自定义的 trie 类型。使用 trie 会占用更多内存,并且由于额外的间接、缓存未命中等原因,查找速度也会变慢。如果您在 IntMap 中严格和取消装箱字段,您可能能够弥补很多差异,但我不确定这是可能的。尝试严格化 BitTrie 中的字段会导致运行时崩溃。

纯 haskell 记忆代码可能很好,但我认为它不如做不安全的事情快(至少在引擎盖下)。你可以申请Lennart Augustsson's technique 看看它在记忆方面的表现是否更好。

【讨论】:

    【解决方案2】:

    减慢 Haskell 的一件事是 IO,Haskell 中的 String 类型提供了 UTF8 支持,而 SPOJ 不需要。 ByteString 的速度非常快,因此您可能需要考虑使用它们。

    【讨论】:

      猜你喜欢
      • 2012-03-14
      • 1970-01-01
      • 2012-06-16
      • 1970-01-01
      • 2020-08-08
      • 1970-01-01
      • 2020-10-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多