Programmer's Note

コード読み書きの備忘録。

Haskell/Clojureで数学パズル:コインの両替

いやあ「プログラマ脳を鍛える数学パズル」楽しいですわ。 まだ5問しかやっていないけど、Clojure/Haskell両方で解いているので、一粒で二度美味しい。 しかも、解説は違う視点の解法を示してくれるので、3度も4度も美味しい感じだ。

5問目は、お金の両替問題で、これシンプルだけど奥が深い。 1000円を10円、50円、100円、500円に両替する。同じコインは何枚使ってもいいが、最大コイン数は15枚。 この時の両替パターン数を求める。

名著SICPにも内容が少し違う両替問題が出ていて、再帰の威力を見せつけられたな。

さて、この問題、まず最初にぱっと思いつく方法は、ベタに組み合わせと条件判定。

Haskellの回答例は、

change_coin :: Int -> Int -> Int
change_coin money max_coins = 
    sum [1 | m10 <- [0..max_coins],
             m50 <- [0..max_coins],
             m100 <- [0..max_coins],
             m500 <- [0..max_coins],
             m10 + m50 + m100 + m500 <= max_coins,
             money == (10*m10 + 50*m50 + 100*m100 + 500*m500)]

main :: IO ()
main = do print $ change_coin 1000 15 

リスト内包表記、便利で簡潔で美しいですわ。

似た簡潔さでClojureも書ける。for文を使う。

(defn change_coin [money max_coins]
  (reduce +
          (for [m10 (range 0 (inc max_coins))
                m50 (range 0 (inc max_coins))
                m100 (range 0 (inc max_coins))
                m500 (range 0 (inc max_coins))
                :when (and (<= (+ m10 m50 m100 m500) max_coins)
                           (= money (+ (* 10 m10)
                                       (* 50 m50)
                                       (* 100 m100)
                                       (* 500 m500))))
                ]
            1)))

(println (change_coin 1000 15))

少しだけ考えて、再帰を使って汎用的な関数を作ってみる。

Haskell版:

change_coin :: Int -> [Int] -> Int -> Int
change_coin 0 _ _ = 1
change_coin _ _ 0 = 0
change_coin _ [] _ = 0
change_coin money (x:xs) max_coins
    = sum [ change_coin (money - x * use) xs (max_coins - use)
            | use <- [0..max_coins]]

main :: IO ()
main = do print $ change_coin 1000 [10,50,100,500] 15

考え方としては、使うコインのリスト[10,50,100,500]を与えて、前からコインを取り出して、 (この場合だとまずは10円)

  • コインを0枚使う → 1000-0 に対して残ったコインで両替する
  • コインを1枚使う → 1000-10 に対して残ったコインで両替する
  • ・・・
  • コインを15枚使う → 1000-10*15 に対して残ったコインで両替する

というのを再帰でやる。(まあ15枚使ったら、もう終わりだけどね) 最後に残金が0なら、うまく換金できたので1を返し、 それ以外でコインのリストが空になったり、最大コイン数使ったら0を返す。

Clojureも同じように組める。

(defn change_coin_patterns [money coins max-coins]
  (cond (zero? money) 1
        (empty? coins) 0
        (zero? max-coins) 0
        :else (reduce + 
                      (for [uses (range 0 (inc max-coins))]
                        (change_coin_patterns (- money (* uses (first coins)))
                                              (rest coins)
                                              (- max-coins uses))))))

(println (change_coin_patterns 1000 [10 50 100 500] 15))

Haskellだとパターンマッチングを使った記述がビューティフル。