📚

Haskellで蟻本やるぜ5(DP)

2020/11/08に公開

前回の続き。
もはやLVS7K'S BLOGの写経になりつつあるが。
今回からDP(動的計画法)をやるぜ。
サンプルデータは、ここから借用したぜ。

2-3 ナップサック問題

毎度、ここをほぼパクリにしたお。

-- https://lvs7k.github.io/posts/2018/11/pccb-easy-3/
-- https://download-takeshi.hatenablog.com/entry/20100301/1267476665
{-# LANGUAGE BangPatterns, FlexibleContexts #-}

import Control.Monad
import Control.Monad.ST
import Data.Array.ST
import Data.Array
import Data.Time

-- 01 ナップサック問題
q1 :: Int -> Int -> [(Int, Int)] -> Int
q1 n m wvs = go n m
  where
    wva = listArray (1, n) wvs
    -- 蟻本のiを増やしていくパターン
    memo = listArray ((0, 0), (n, m)) [go i j | i <- [0 .. n], j <- [0 .. m]]
    go 0 _ = 0
    go i j
        | j < w     = x1        -- 荷物が入らない
        | otherwise = max x1 x2 -- 入れる場合と入れない場合の両方を試す
      where
        (w, v) = wva ! i
        -- 荷物を入れる場合
        x1 = memo ! (i - 1, j)
        -- 荷物を入れない場合
        x2 = memo ! (i - 1, j - w) + v

q1' :: Int -> Int -> [(Int, Int)] -> Int
q1' n m wvs = runST $ do
    dp <- newArray ((0, 0), (n, m)) 0 :: ST s (STUArray s (Int, Int) Int)
    forM_ [1 .. n] $ \i -> do
        -- 荷物データから重さと価値を取得
        let (w, v) = wva ! i
        -- dp
        forM_ [0 .. m] $ \j -> do
            x1 <- readArray dp (i - 1, j)
            if j < w then
                writeArray dp (i, j) x1
            else do
                x2 <- readArray dp (i - 1, j - w)
                writeArray dp (i, j) (max x1 (x2 + v))
    readArray dp (n, m)
  where
    -- 荷物データ
    wva = listArray (1, n) wvs

q1'' :: Int -> Int -> [(Int, Int)] -> Int
q1'' n m wvs = runST $ do
    dp   <- newArray ((0, 0), (n, m)) 0     :: ST s (STUArray s (Int, Int) Int)
    memo <- newArray ((0, 0), (n, m)) False :: ST s (STUArray s (Int, Int) Bool)
    -- sequence_ は、(モナドの)リストを取り、要素を順に実行して、値は返さない関数
    -- memoがn=0のときは、0を返させるよう初期化(True:計算済みにしておく)
    sequence_ [writeArray memo (0, i) True | i <- [0 .. m]]
    go dp memo (n, m)
  where
    wva = listArray (1, n) wvs
    go dp memo (i, j) = do
        exists <- readArray memo (i, j)
        -- まだ計算していない場合
        when (not exists) $ do
            let (w, v) = wva ! i
            x1 <- go dp memo (i - 1, j)
            if j < w
                then do
                    writeArray dp (i, j) x1
                else do
                    x2 <- go dp memo (i - 1, j - w)
                    writeArray dp (i, j) (max x1 (x2 + v))
            writeArray memo (i, j) True
        readArray dp (i, j)


main :: IO ()
main = do
    x <- getCurrentTime
    -- let ret = q1 4 5 [(2,3),(1,2),(3,4),(2,2)]
    let ret = q1 10 300 [(168,496),(10,45),(145,325),(60,347),(10,61),(124,486),(124,446),(105,22),(126,110),(184,475)]
    print ret
    y <- getCurrentTime
    print $ diffUTCTime y x
    x' <- getCurrentTime
    --let ret' = q1' 4 5 [(2,3),(1,2),(3,4),(2,2)]
    let ret' = q1' 10 300 [(168,496),(10,45),(145,325),(60,347),(10,61),(124,486),(124,446),(105,22),(126,110),(184,475)]
    print ret'
    y' <- getCurrentTime
    print $ diffUTCTime y' x'
    x'' <- getCurrentTime
    --let ret'' = q1'' 4 5 [(2,3),(1,2),(3,4),(2,2)]
    let ret'' = q1'' 10 300 [(168,496),(10,45),(145,325),(60,347),(10,61),(124,486),(124,446),(105,22),(126,110),(184,475)]
    print ret''
    y'' <- getCurrentTime
    print $ diffUTCTime y'' x''
    x <- getCurrentTime
    -- let ret = q1 4 5 [(2,3),(1,2),(3,4),(2,2)]
    let ret = q1 10 300 [(168,496),(10,45),(145,325),(60,347),(10,61),(124,486),(124,446),(105,22),(126,110),(184,475)]
    print ret
    y <- getCurrentTime
    print $ diffUTCTime y x

q1 が純粋にHaskellでメモ化しながら説いている
q1' はDPを使っている
q1''はDPとメモ化の両方を使っている

mainで時間測定しつつ、q1とq1'とq1''を実行している。
メモリキャッシュの関係か、2回目の実行が早いので、q1は2回実行している。
q1よりもDPを使っているq1'の方が速いぜ!と思っていたら違った。

$ ./sample
1038
0.000669s
1038
0.002878s
1038
0.000464s
1038
0.0002s

ということで、プロファイルを取ってみる。
それぞれの関数を別ファイルにして、実行してみた。

q1 純情メモ

$ stack ghc sample1.hs
$ ./sample1 +RTS -s
1038
0.000748s
         860,480 bytes allocated in the heap
           3,480 bytes copied during GC
          44,576 bytes maximum residency (1 sample(s))
          25,056 bytes maximum slop
               0 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0         0 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s
  Gen  1         1 colls,     0 par    0.000s   0.000s     0.0002s    0.0002s

  INIT    time    0.000s  (  0.003s elapsed)
  MUT     time    0.000s  (  0.001s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.011s elapsed)
  Total   time    0.001s  (  0.015s elapsed)

  %GC     time       0.0%  (0.0% elapsed)

  Alloc rate    1,964,566,210 bytes per MUT second

  Productivity  51.6% of total user, 6.1% of total elapsed

まぁ、こんなもんでしょ

DP

$ stack ghc sample2.hs
$ ./sample2 +RTS -s
1038
0.003429s
       7,447,424 bytes allocated in the heap
          21,008 bytes copied during GC
          56,640 bytes maximum residency (2 sample(s))
          29,152 bytes maximum slop
               0 MB total memory in use (0 MB lost due to fragmentation)

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

  INIT    time    0.000s  (  0.002s elapsed)
  MUT     time    0.003s  (  0.003s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.008s elapsed)
  Total   time    0.003s  (  0.014s elapsed)

  %GC     time       0.0%  (0.0% elapsed)

  Alloc rate    2,763,422,634 bytes per MUT second

  Productivity  82.6% of total user, 24.0% of total elapsed

heapメモリを7.4MBも使っているお。

DP+メモ化

$ stack ghc sample3.hs
$ ./sample3 +RTS -s
1038
0.001582s
       1,263,504 bytes allocated in the heap
          10,200 bytes copied during GC
          44,576 bytes maximum residency (1 sample(s))
          29,152 bytes maximum slop
               0 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0         1 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s
  Gen  1         1 colls,     0 par    0.000s   0.000s     0.0002s    0.0002s

  INIT    time    0.000s  (  0.003s elapsed)
  MUT     time    0.001s  (  0.002s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.009s elapsed)
  Total   time    0.001s  (  0.014s elapsed)

  %GC     time       0.0%  (0.0% elapsed)

  Alloc rate    1,294,573,770 bytes per MUT second

  Productivity  68.4% of total user, 12.0% of total elapsed

メモリ使用量は、1.2MBでましになった。

メモ化だけで、DPに勝てるのはうれしい。
Haskellで、単純にDPにするとArray使うから手続き型で書いているのと変わらん。
しかし、おかしいなぁ。
データが少ないからかなぁ。
続きを書いたぜ。
大量データの場合は、結果が変わったぜ。

Discussion