Haskell 的 Knapsack 解决方案“超出时间限制”

has*_*eat 6 algorithm haskell functional-programming

这是 Kattis 提出的一个非常标准的背包问题。下面是 Haskell 中一个简单的动态编程解决方案:

{-# Language OverloadedStrings #-}

import Control.Arrow ((>>>))
import Data.List (intercalate)
import Data.Array
import Data.Maybe
import qualified Data.ByteString.Lazy.Char8 as C

main = C.interact solve

solve = C.words >>> fmap readInt >>> divideInput
        >>> fmap (solveCase >>> toBS)
        >>> C.unlines
  where readInt = C.readInt >>> fromJust >>> fst

divideInput :: [Int] -> [[Int]]
divideInput [] = []
divideInput (c:n:ls) = (c : n : this) : divideInput that
  where (this, that) = splitAt (2*n) ls

solveCase :: [Int] -> [[Int]]
solveCase (c:n:os) = [[length is], is]
  where is = recover (n, c) []

        recover (i, j) rs | table ! (i, j) == 0 = rs
                          | table ! (i, j) == table ! (i-1, j) =
                            recover (i-1, j) rs
                          | table ! (i, j) == vi + (table ! (i-1, j-wi)) =
                            recover (i-1, j-wi) ((i-1):rs)
          where (vi, wi) = objs ! i

        objs :: Array Int (Int, Int)
        objs = listArray (1, n) $ pairs os
        pairs [] = []
        pairs (v:w:os) = (v,w) : pairs os

        -- table[i][j] is the max value that can be achieved with
        -- objects [1..i] where the total weight of selected
        -- objects is <= j.
        table :: Array (Int, Int) Int
        table = array bnds [(ij, fill ij) | ij <- range bnds]
          where 
            bnds = ((0,0), (n,c))
            fill (i, w) | i == 0 || w == 0 = 0
                        | w < wi = vx
                        | otherwise = max vx (vy+vi)
              where vx = table ! (i-1, w)
                    vy = table ! (i-1, w - wi)
                    (vi, wi) = objs ! i

toBS :: [[Int]] -> C.ByteString
toBS [[n], is] = C.intercalate "\n"
                 [C.pack (show n), C.intercalate " " $ C.pack . show <$> is]
Run Code Online (Sandbox Code Playgroud)

然而,一旦提交给 Kattis,代码就会给出 TLE,考虑到其O(Cn)复杂度(从最大容量C的n 个对象中选取),这似乎令人惊讶。有人对如何解决这个问题有任何建议吗?

我已经尝试过在 ST monad 中使用可变数组。但可变数组在这里没有帮助,这并不奇怪,因为 DP 数组永远不需要更新。

在 C=2000、n=2000 上对其进行分析,值和权重均匀地在 1 到 20000 之间随机选取。大约需要 1.16 秒。完整简介如下:

     523,035,224 bytes allocated in the heap
     598,289,064 bytes copied during GC
     144,045,528 bytes maximum residency (4 sample(s))
         662,056 bytes maximum slop
             254 MiB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       464 colls,     0 par    0.374s   0.394s     0.0008s    0.0196s
  Gen  1         4 colls,     0 par    0.141s   0.202s     0.0505s    0.1067s

  INIT    time    0.000s  (  0.004s elapsed)
  MUT     time    0.651s  (  0.664s elapsed)
  GC      time    0.515s  (  0.596s elapsed)
  EXIT    time    0.000s  (  0.001s elapsed)
  Total   time    1.166s  (  1.265s elapsed)

  %GC     time       0.0%  (0.0% elapsed)

  Alloc rate    804,028,826 bytes per MUT second

  Productivity  55.8% of total user, 52.5% of total elapsed
Run Code Online (Sandbox Code Playgroud)

has*_*eat 4

刚刚设法使用 IOUArray 和 IOArray 获得了可接受的变体。还需要调整代码以尽可能减少列表的使用。1.61 秒被接受。

我很早就尝试过 STUArray/STArray,并认为它们会提供与 IOUArray/IOArray 相同的性能。然而,事实证明,即使基于 STUArray/STArray 的解决方案使用的内存和时间少于功能数组,它仍然无法通过 TLE 的最后一个测试文件。

我看到最快接受的 Haskell 解决方案只用了 0.38 秒。我很好奇他们做了什么才能让它这么快。下面附有我接受的代码以及一些个人资料信息。欢迎任何进一步提高其性能的想法。

{-# Language OverloadedStrings #-}

import Control.Arrow ((>>>))
import Control.Monad
import Data.List (intercalate)
import Data.Array.IO
import Data.Maybe
import qualified Data.ByteString.Lazy.Char8 as C
  
main = do
  inStr <- C.getContents
  solveCases $ (C.words >>> fmap readInt) inStr
  where readInt = C.readInt >>> fromJust >>> fst

solveCases :: [Int] -> IO ()
solveCases [] = return ()
solveCases (c:n:os) = do
  objs <- newArray (1, n) (0,0)
  os' <- fillObj objs n os
  table <- buildTable objs
  indices <- recover table objs n c []
  putStrLn $ show $ length indices
  putStrLn $ intercalate " " $ show <$> indices
  solveCases os'
  where 
    recover :: IOUArray (Int, Int) Int ->
               IOArray Int (Int, Int) ->
               Int -> Int -> [Int] -> IO [Int]
    recover table objs i j rs = do
      v <- readArray table (i, j)
      if (v == 0)
        then return rs
        else do
        v' <- readArray table (i-1, j)
        if (v == v')
          then recover table objs (i-1) j rs
          else do
          (_, wi) <- readArray objs i
          recover table objs (i-1) (j-wi) ((i-1):rs)
          
    fillObj :: IOArray Int (Int, Int) -> Int -> [Int] -> IO [Int]
    fillObj objs n vws = go 1 vws
      where go :: Int -> [Int] -> IO [Int]
            go i (v:w:vws) | i == n = writeArray objs i (v, w) >> return vws
                           | otherwise = do
                               writeArray objs i (v, w)
                               go (i+1) vws

    bnds = ((0,0), (n,c))

    -- table[i][j] is the max value that can be achieved with
    -- objects [0..i] such that the max selected weight is <= j.
    buildTable :: IOArray Int (Int, Int) -> IO (IOUArray (Int, Int) Int)
    buildTable objs = do
      table <- newArray bnds 0
      forM_ (range bnds) $ \(i, w) -> do
        when (i > 0 && w > 0) $ do
          (vi, wi) <- readArray objs i
          vx <- readArray table (i-1, w)
          if (w < wi)
            then do
            writeArray table (i, w) vx
            else do
            vy <- readArray table (i-1, w-wi)
            writeArray table (i, w) $ max vx (vy+vi)
      return table
Run Code Online (Sandbox Code Playgroud)
      33,901,296 bytes allocated in the heap
         183,504 bytes copied during GC
          53,320 bytes maximum residency (1 sample(s))
          36,792 bytes maximum slop
              33 MiB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0         2 colls,     0 par    0.000s   0.000s     0.0001s    0.0002s
  Gen  1         1 colls,     0 par    0.000s   0.003s     0.0025s    0.0025s

  INIT    time    0.000s  (  0.004s elapsed)
  MUT     time    0.040s  (  0.048s elapsed)
  GC      time    0.000s  (  0.003s elapsed)
  EXIT    time    0.000s  (  0.006s elapsed)
  Total   time    0.040s  (  0.061s elapsed)

  %GC     time       0.0%  (0.0% elapsed)

  Alloc rate    847,130,013 bytes per MUT second

  Productivity  98.9% of total user, 79.3% of total elapsed
Run Code Online (Sandbox Code Playgroud)