N-Queens問題は8-Queens問題の一般形で、 N x N のチェスのボード上にN個のクイーンが互いに攻撃しあわないような配置を求める問題です。バックトラッキングアルゴリズムで解くと非常に効率よく解ける例として有名です。
今回はHaskellでlist monadを使って解いてみましょう。List monadを用いることで簡単に楽しく書けるという利点があります。
動機は、いろいろ調べたのですが、Haskellで単純で速そうなものが見つからなかったためです。
アルゴリズムとしては当然バックトラッキング。
クイーンの配置を表すデータ構造としては [Int] を用いましょう。[(Int, Int)]としてしまうと冗長な感があります。経験上データ構造はなるべくダイエットして必要十分に近いものが理想的だと思います。そのほうが簡単に楽しく書けます。
まずlist monadの定義を確認しましょう。List は Monad型クラスのインスタンスです。その定義は
GHC.Baseに記載されています。
instance Monad [] where
m >>= k = foldr ((++) . k) [] m
m >> k = foldr ((++) . (\ _ -> k)) [] m
return x = [x]
fail _ = []
bind関数>>= がfoldrを使って書かれていますが、xs >>= f = concat (map f xs)と同じことです。
以上から書いてみると、こんな感じになりました。
import Data.List
type Queens = [Int]
nQueens :: Int -> [Queens]
nQueens n = solve [1..n] []
solve :: Queens -> Queens -> [Queens]
solve [] ys = return ys
solve xs ys =
xs >>= func
where
func x
| isOK (x:ys) = solve (delete x xs) (x:ys)
| otherwise = fail "failed!"
isOK :: Queens -> Bool
isOK [] = True
isOK (q:qs) = (all (/= 0) . zipWith (-) [1..(length qs)] . (map (abs . (-) q))) qs
速度計測
比較対象
・
LiteratePrograms.org
・
pi8027
上のLiteratePrograms.org(以下LPと呼称)のものは同じくバックトラッキングとリストモナドを用いたもの、下のものはしらみ潰し探索法を用いたものです。しらみつぶし探索法は一般には遅いのですが、とてもコーディングしやすいという利点があります。
計測結果(単位:sec)
N | Etsukata | LP | pi8027 |
9 | 0.23 | 0.73 | 2.73 |
10 | 1.06 | 3.84 | 27.33 |
11 | 5.44 | 23.83 | -- |
なんで速いの?
しらみ潰し探索法よりもバックトラッキングのほうが速いのは明らかです。
LPよりも自分のものが速いのは、確かめる必要の無い組み合わせを予め省いているからです。メモリの使用量も半分以下で済みます。
もっと速くてわかりやすい書き方がある場合は是非おしえてください!