The Knight's Tour: solutions please

Lee Pike forwarded the following: "Solving the Knight's Tour Puzzle In 60 Lines of Python" http://developers.slashdot.org/article.pl?sid=08/11/30/1722203 Seems that perhaps (someone expert in) Haskell could do even better? Maybe even parallelize the problem? :) So, team, anyone want to implement a Knight's Tour solver in a list monad/list comprehension one liner? These little puzzles are made for fast languages with backtracking monads.... Post solutions to the wiki.. -- Don

G'day all.
Quoting Don Stewart
So, team, anyone want to implement a Knight's Tour solver in a list monad/list comprehension one liner? These little puzzles are made for fast languages with backtracking monads....
I conjecture that any one-liner won't be efficient. Anyway, here's my ~30 min attempt. The showBoard and main are both very quick and dirty, and I'm sure someone can do much better. I particularly like the fact that changing "Maybe" to "[]" will make knightsTour return all tours starting at the upper left-hand corner, rather than just one. Warm fuzzy things rule. Cheers, Andrew Bromage module Main where import qualified Data.Set as S import Data.List import Data.Function import Control.Arrow import Control.Monad import System knightsTour :: Int -> Maybe [(Int,Int)] knightsTour size = tour [(0,0)] (S.fromAscList [ (x,y) | x <- [0..size-1], y <- [0..size-1], x /= 0 || y /= 0 ]) where jumps = [(2,1),(1,2),(2,-1),(-1,2),(-2,1),(1,-2),(-2,-1),(-1,-2)] tour moves@(pos:_) blank | S.null blank = return (reverse moves) | otherwise = msum [ tour (npos:moves) (npos `S.delete` blank) | npos <- nextPositions pos ] where nextPositions = map snd . sortBy (compare `on` fst) . map (length . neighbours &&& id) . neighbours neighbours (x,y) = [ npos | (x',y') <- jumps, let { npos = (x+x',y+y') }, npos `S.member` blank ] showBoard :: Int -> [(Int,Int)] -> ShowS showBoard size = inter bdr . map (inter ('|':) . map (shows . fst)) . groupBy ((==) `on` fst.snd) . sortBy (compare `on` snd) . zip [1..] where bdr = ('\n':) . inter ('+':) (replicate size (replicate width '-' ++)) . ('\n':) width = length . show $ size*size pad s = \r -> replicate (width - length (s "")) ' ' ++ s r inter sep xs = sep . foldr (.) id [ pad x . sep | x <- xs ] main :: IO () main = do a <- getArgs size <- case a of [] -> return 8 (s:_) -> return (read s) putStrLn $ case knightsTour size of Nothing -> "No solution found." Just b -> showBoard size b ""

ajb:
G'day all.
Quoting Don Stewart
: So, team, anyone want to implement a Knight's Tour solver in a list monad/list comprehension one liner? These little puzzles are made for fast languages with backtracking monads....
I conjecture that any one-liner won't be efficient.
Anyway, here's my ~30 min attempt. The showBoard and main are both very quick and dirty, and I'm sure someone can do much better.
I particularly like the fact that changing "Maybe" to "[]" will make knightsTour return all tours starting at the upper left-hand corner, rather than just one. Warm fuzzy things rule.
Cheers, Andrew Bromage
module Main where
import qualified Data.Set as S import Data.List import Data.Function import Control.Arrow import Control.Monad import System
knightsTour :: Int -> Maybe [(Int,Int)] knightsTour size = tour [(0,0)] (S.fromAscList [ (x,y) | x <- [0..size-1], y <- [0..size-1], x /= 0 || y /= 0 ]) where jumps = [(2,1),(1,2),(2,-1),(-1,2),(-2,1),(1,-2),(-2,-1),(-1,-2)] tour moves@(pos:_) blank | S.null blank = return (reverse moves) | otherwise = msum [ tour (npos:moves) (npos `S.delete` blank) | npos <- nextPositions pos ] where nextPositions = map snd . sortBy (compare `on` fst) . map (length . neighbours &&& id) . neighbours neighbours (x,y) = [ npos | (x',y') <- jumps, let { npos = (x+x',y+y') }, npos `S.member` blank ]
showBoard :: Int -> [(Int,Int)] -> ShowS showBoard size = inter bdr . map (inter ('|':) . map (shows . fst)) . groupBy ((==) `on` fst.snd) . sortBy (compare `on` snd) . zip [1..] where bdr = ('\n':) . inter ('+':) (replicate size (replicate width '-' ++)) . ('\n':) width = length . show $ size*size pad s = \r -> replicate (width - length (s "")) ' ' ++ s r inter sep xs = sep . foldr (.) id [ pad x . sep | x <- xs ]
main :: IO () main = do a <- getArgs size <- case a of [] -> return 8 (s:_) -> return (read s) putStrLn $ case knightsTour size of Nothing -> "No solution found." Just b -> showBoard size b ""
dolio implemented a cute one based on continuations, that's also about 10x faster than the python version, http://hpaste.org/12546#a2 -- Don

Here's a clean-up of my code (it even fits within the line-length limit of my mail client :)). Note that it's pretty much exactly the Python algorithm. When the Python program finds a solution, it prints the board and exits. Since that's evil IO type stuff, we noble functional folk instead set up an exit continuation using callCC, and call it when we find a solution. :) I haven't bothered testing it against the Python version, but the backtracking solution I wrote with the Logic monad (and Data.Map) took around 50% more time than this. -- Dan ---- snip ---- module Main where import Control.Monad.Cont import Control.Monad.ST import Data.Array.ST import Data.List import Data.Ord import Data.Ix import System.Environment type Square = (Int, Int) type Board s = STUArray s (Int,Int) Int type ChessM r s = ContT r (ST s) type ChessK r s = String -> ChessM r s () successors :: Int -> Board s -> Square -> ChessM r s [Square] successors n b = sortWith (fmap length . succs) <=< succs where sortWith f l = map fst `fmap` sortBy (comparing snd) `fmap` mapM (\x -> (,) x `fmap` f x) l succs (i,j) = filterM (empty b) [ (i', j') | (dx,dy) <- [(1,2),(2,1)] , i' <- [i+dx,i-dx] , j' <- [j+dy, j-dy] , inRange ((1,1),(n,n)) (i',j') ] empty :: Board s -> Square -> ChessM r s Bool empty b s = fmap (<1) . lift $ readArray b s mark :: Square -> Int -> Board s -> ChessM r s () mark s k b = lift $ writeArray b s k tour :: Int -> Int -> ChessK r s -> Square -> Board s -> ChessM r s () tour n k exit s b | k > n*n = showBoard n b >>= exit | otherwise = successors n b s >>= mapM_ (\x -> do mark x k b tour n (k+1) exit x b -- failed, rollback mark x 0 b) showBoard :: Int -> Board s -> ChessM r s String showBoard n b = fmap unlines . forM [1..n] $ \i -> fmap unwords . forM [1..n] $ \j -> pad `fmap` lift (readArray b (i,j)) where k = floor . log . fromIntegral $ n*n pad i = let s = show i in replicate (k-length s) ' ' ++ s main = do (n:_) <- map read `fmap` getArgs s <- stToIO . flip runContT return $ (do b <- lift $ newArray ((1,1),(n,n)) 0 mark (1,1) 1 b callCC $ \k -> tour n 2 k (1,1) b >> fail "No solution!") putStrLn s

dan.doel:
Here's a clean-up of my code (it even fits within the line-length limit of my mail client :)). Note that it's pretty much exactly the Python algorithm. When the Python program finds a solution, it prints the board and exits. Since that's evil IO type stuff, we noble functional folk instead set up an exit continuation using callCC, and call it when we find a solution. :)
I haven't bothered testing it against the Python version, but the backtracking solution I wrote with the Logic monad (and Data.Map) took around 50% more time than this.
I've created a wiki page, http://haskell.org/haskellwiki/The_Knights_Tour I note the LogicT version is the shortest so far. -- Don

Don Stewart wrote:
Lee Pike forwarded the following:
"Solving the Knight's Tour Puzzle In 60 Lines of Python"
http://developers.slashdot.org/article.pl?sid=08/11/30/1722203
Seems that perhaps (someone expert in) Haskell could do even better? Maybe even parallelize the problem? :)
As one of the posters there points out, for n=100 the program doesn't actually backtrack if the 'loneliest neighbour' heuristic is used. Do any of our programs finish quickly for n=99? The Python one doesn't. Bertram

On Monday 01 December 2008 1:39:13 pm Bertram Felgenhauer wrote:
As one of the posters there points out, for n=100 the program doesn't actually backtrack if the 'loneliest neighbour' heuristic is used. Do any of our programs finish quickly for n=99? The Python one doesn't.
Nothing I tried finished. Do you have any figures on how much backtracking needs to be done to find a solution for n=99 (there is a solution, right?)? I tweaked the continuation version to print k when it backtracks, and it continuously spit out numbers around 9790. I get the feeling it doesn't matter how fast your backtracking infrastructure is in this case as long as you use the same general algorithm. On a long shot, I even tried using Logic's alternate bind based on fair choice, but that didn't seem to be any better. -- Dan

Dan Doel wrote:
On Monday 01 December 2008 1:39:13 pm Bertram Felgenhauer wrote:
As one of the posters there points out, for n=100 the program doesn't actually backtrack if the 'loneliest neighbour' heuristic is used. Do any of our programs finish quickly for n=99? The Python one doesn't.
Nothing I tried finished. Do you have any figures on how much backtracking needs to be done to find a solution for n=99 (there is a solution, right?)? I tweaked the continuation version to print k when it backtracks, and it continuously spit out numbers around 9790. I get the feeling it doesn't matter how fast your backtracking infrastructure is in this case as long as you use the same general algorithm.
On a long shot, I even tried using Logic's alternate bind based on fair choice, but that didn't seem to be any better.
FWIW, fair choice (interleave) is much slower than unfair choice (mplus) in logict. Unfortunately this means you need to know a lot about the problem domain to correctly choose between them when maximal performance is at stake; just using fair choice everywhere costs too much for many problems. -- Live well, ~wren

Dan Doel wrote:
On Monday 01 December 2008 1:39:13 pm Bertram Felgenhauer wrote:
As one of the posters there points out, for n=100 the program doesn't actually backtrack if the 'loneliest neighbour' heuristic is used. Do any of our programs finish quickly for n=99? The Python one doesn't.
Nothing I tried finished. Do you have any figures on how much backtracking needs to be done to find a solution for n=99 (there is a solution, right?)?
Yes, there is a solution. After changing successors n b = sortWith (length . succs) . succs to successors n b = sortWith (length . (succs =<<) . succs) . succs in the LogicT solution, it finds one with no backtracking at all. This heuristic fails on other n though, n=8 and n=66 at least. The obvious next step, successors n b = sortWith (length . (succs =<<) . (succs =<<) . succs) . succs works without backtracking up to n=100. These improved heuristics don't come cheap though. Here are some timings for n = 100: LogicT : 0.48 user 0.00 system 0:00.48 elapsed LogicT' : 2.16 user 0.00 system 0:02.16 elapsed LogicT'': 13.84 user 0.01 system 0:13.86 elapsed Bertram

G'day all.
Quoting Bertram Felgenhauer
successors n b = sortWith (length . succs) . succs [...] successors n b = sortWith (length . (succs =<<) . succs) . succs [...] successors n b = sortWith (length . (succs =<<) . (succs =<<) . succs) . succs [...] These improved heuristics don't come cheap though.
The heuristic is cheaper and more useful when the ply of the tree is low. It's more expensive and less useful when the ply is high. Moreover, deeper neighbour searches may only be useful in cases where the shallower searchers fail to settle on the best course of action. So something like the following might be better. Note that "d" here is an example only; I don't promise it's good. successors n b = sortWith heuristic . succs where heuristic p = let ps = succs p d = 5 - length ps `div` 2 in map length . take d . iterate (succs =<<) $ ps One more thing: Deeper neighbour searches are also unnecessarily expensive because they recompute "succs" a lot. It seems to me that if you memoed this, what you'd actually have is an explicit lazy search tree data structure. Hint hint. Cheers, Andrew Bromage
participants (5)
-
ajb@spamcop.net
-
Bertram Felgenhauer
-
Dan Doel
-
Don Stewart
-
wren ng thornton