Haskellで幅優先探索
Haskellで幅優先探索をしようとして、 キューをどうするか困ったことはないでしょうか?
まず思いつくだろうことは、Data.Sequence.Seqをキューとして使って、キューSeq node
を状態として持ち回れば、手続き型言語で普段書いていたような幅優先探索が書ける、ということでしょう。
もちろん、Mutableな配列をキューにしてもできます。IO
やST
を探索以外に使わなければならない事情があればいい選択肢です。
しかし、キューを明示的に使わなくてもできる方法がいくつかあります。それぞれ面白みがあるので紹介していきます。
例題:コインで支払い
次の例題を考えます。
この国では、金貨1枚は銀貨3枚に、プラチナ貨1枚は銀貨7枚に、ダイヤモンド貨1枚は銀貨19枚に相当します。銀貨100枚の価値がある商品を、なるべく少ない枚数の銀貨以外のコインで買う方法を見つけなさい。
(DPのほうが簡単というのは置いておいて、)この問題は、合計金額をノード、コイン1枚追加を辺とした有向非巡回グラフを考えて、合計金額100のノードまでのパスを幅優先探索で見つければいいですね。
この問題はHaskellで次のように書けます。
type Coin = Int
type Value = Int
type Graph label node = node -> [(label, node)]
addCoin :: Graph Coin Value
!n = [ (c,n+c) | c <- [3,7,19], n+c <= 100 ] addCoin
キュー(ここではSeq
)を使う
import Data.Foldable
import Data.Monoid (First(..))
import qualified Data.Sequence as S
import Data.Sequence (Seq(Empty, (:<|), (:|>)))
type Tree node = node -> [node]
bfSeq :: Tree node -> node -> [node]
= loop (S.singleton root)
bfSeq step root where
Empty = []
loop :<| q) = n : loop (foldl' (:|>) q (step n))
loop (n
graphToPaths :: Graph label node -> Tree ([label], node)
graphToPaths graph (path, node)= [ (l : path, node') | (l, node') <- graph node ]
addCoin' :: Tree ([Coin], Value)
= graphToPaths addCoin
addCoin'
firstJust :: Foldable t => (a -> Maybe b) -> t a -> Maybe b
= getFirst . foldMap (First . f)
firstJust f
addCoin' :: Tree ([Coin], Value)
= graphToPaths addCoin
addCoin'
solvePuzzleSeq :: Maybe [Coin]
= firstJust f $ bfSeq addCoin' ([], 0)
solvePuzzleSeq where f (path, 100) = Just (reverse path)
= Nothing f _
余再帰を使う
bfList :: Tree node -> node -> [node]
=
bfList step root let ans = root : go 1 ans
in ans
where
| n <= 0 = []
go n _ :xs) =
go n (xlet children = step x
in children ++ go (n - 1 + length children) xs
= error "Never reach here"
go _ []
solvePuzzleList :: Maybe [Coin]
= firstJust f $ bfList addCoin' ([], 0)
solvePuzzleList where f (path, 100) = Just (reverse path)
= Nothing f _
ans = root : go 1 ans
のように再帰的に構築されたリストが、
まるでキューのように扱えます。
遅延データ構造を使う
次のLazy
型を考えます。Lazy a
は、a
型の値が
- 計算できなかった …
Fail
- 計算終了した …
Ok a
- 計算中である …
Next x
という3つの状態を表します。forceLazy
は、
これを単なる失敗または成功としてMaybe a
に単純化します。
data Lazy a = Fail | Ok a | Next (Lazy a)
deriving (Functor, Foldable, Traversable)
forceLazy :: Lazy a -> Maybe a
Fail = Nothing
forceLazy Ok a) = Just a
forceLazy (Next x) = forceLazy x forceLazy (
Applicative
とMonad
のインスタンスが次のように定義できます。
instance Applicative Lazy where
pure = Ok
<*>) = ap
(
instance Monad Lazy where
return = pure
Fail >>= _ = Fail
Ok a >>= k = k a
Next x >>= k = Next (x >>= k)
さらに、“2つのLazy a
のうち先に完了したほうを返す”を(<|>)
として、
Alternative
にもなります。
instance Alternative Lazy where
= Fail
empty Fail <|> y = y
Ok a <|> _ = Ok a
<|> Fail = x
x <|> Ok a = Ok a
_ Next x <|> Next y = Next (x <|> y)
これを使って、BFSのようなものが実装できます。
bfSearchLazy :: Tree node -> node -> (node -> Lazy a) -> Lazy a
= go root
bfSearchLazy step root goal where
= goal x <|> Next (asum $ fmap go (step x))
go x
solvePuzzleLazy :: Maybe [Coin]
= forceLazy $ bfSearchLazy addCoin' ([], 0) f
solvePuzzleLazy where f (path, 100) = Ok (reverse path)
= Fail f _
bfSearchLazy
は、「木の全ノードx
に対して、ルートからの距離に応じたステップ数後にgoal x
を評価する計算をさせ、一番先に完了した計算を採用する」と解釈できます。遅延評価のおかげで、これは幅優先探索と同じ深さまでしか木をたどりませんし、木が無限の深さであっても大丈夫です。