Haskell/Clojureでrepeated_combinationを実装してみる
前回やった Haskell/Clojureで数学パズル:コインの両替 - Programmer's Note の問題5は、書籍の解答例のひとつに、Rubyのrepeated_combination()を使った解があった。
こいつは任意の回数、配列の要素の組み合わせを出してくれる関数で、 探せばClojure/Haskellにもあるだろうけど、ちょうどいい感じの題材なので実装してみる。
任意の大きさのリスト[a,b,c,d,...]
を任意の回数n
、各要素の組み合わせを出す。
使い方
repeat_combination [1 2 3] 2 ; -> ((1 1) (1 2) (1 3) (2 1) (2 2) (2 3) (3 1) (3 2) (3 3)) repeat_combination [1 2 3] 3 ; -> ((1 1 1) (1 1 2) (1 1 3) (1 2 1) (1 2 2) (1 2 3) (1 3 1) (1 3 2) (1 3 3) (2 1 1) (2 1 2) (2 1 3) (2 2 1) (2 2 2) (2 2 3) (2 3 1) (2 3 2) (2 3 3) (3 1 1) (3 1 2) (3 1 3) (3 2 1) (3 2 2) (3 2 3) (3 3 1) (3 3 2) (3 3 3))
アルゴリズムは2通りあって、深さ優先か幅優先か。
いろいろやってみたけれど、下記の実装が一番シンプルでわかりやすいな。 幅優先で処理するやり方。
Haskellの実装:
comb :: [[Int]] -> [[Int]] -> [[Int]] comb xs ys = [ x ++ y | x <- xs, y <- ys] comb_n_times :: [[Int]] -> Int -> [[Int]] comb_n_times xs 1 = xs comb_n_times xs n = comb xs (comb_n_times xs (n-1)) repeat_combination :: [Int] -> Int -> [[Int]] repeat_combination xs 1 = [xs] repeat_combination xs n = let ys = [[x] | x <- xs] in comb_n_times ys n
やっていることは以下のような感じ。
入力データを変換してから [a,b,c] => [[a],[b],[c]] 1回目 [[a],[b],[c]] x [[a],[b],[c]] => [a,a][a,b][a,c][b,a][b,b][b,c][c,a][c,b][c,c] 2回目は1回目の結果にたいして、 [[a],[b],[c]] x [[a,a][a,b][a,c][b,a][b,b][b,c][c,a][c,b][c,c]] => [a,a,a][a,a,b][a,a,c] .... 以上を繰り返す
コードとしては分かりやすいが、SICPでいうrecusive processになっているので、末尾最適化できない。 nが大きいとStack Overflowを起こす。 とはいえ、この幅優先な方法は、iterative processな実装も可能ではある。
深さ優先
一方、深さ優先で組み合わせを作っていくやりかたもある。 プロセスはこんな感じ。
[a,b,c] x [a,b,c] x [a,b,c] a -> a -> a => [a,a,a] -> b => [a,a,b] -> c => [a,a,c] -> b -> a => [a,b,a] -> b => [a,b,b] -> c => [a,b,c] ...
こっちの方はClojureの実装を載せる。
(defn repeat-combination [xs n] (letfn [(comb-depth [cnt res] (if (zero? cnt) [res] (apply concat (for [x xs] (comb-depth (dec cnt) (conj res x)))))) ] (if (= n 1) [xs] (comb-depth n []))))
こっちの方法はあまりメリットはないかな・・・。 iterative processな実装にすることができない(たぶん)。