[Haskell-cafe] Polymorphic Sudoku solver
Chris Kuklewicz
haskell at list.mightyreason.com
Wed May 31 12:32:40 EDT 2006
A while back there was a long thread about Sudoku solvers (some of which ended
up on http://haskell.org/haskellwiki/Sudoku ). I contributed my brute-force
dancing links solver at the time, and mentioned that I had a by-logic solver
that, while a bit slow, was as good as most of those being discussed.
At the time the code for my solver was too ugly to post. Attached is a cleaned
up version.
I have gone back and rewritten it, and come to the conclusion: There are only
two deduction algorithms: "subsets" and "blocks". These subsume the other types
of propagation and deduction. So I made this version as a "minimalist" example
instead of going for performance.
The "subsets" algorithm can be applied to all 6 permutations of row column and
value, as well as 1 special case of value and block indices.
The "blocks" algorithm can be applied 4 ways (in two flavors and to either
permutation of row/column or column/row).
There are newtypes for row, column, value, block index, and sub-block index.
The state is held in an array of type DiffArray (R,C,V) Bool
The actual computation is a series of concat/map/filter/group/sort
operations on the assocs's of the array.
The choice of which permutation is handled by leaning on the type system to
reify the type into appropriate view,shuffle, and unshuffle functions.
It should solve exactly the same number of puzzles as my older version, where "I
sent the 36628 line sudoku17 puzzle through it and it could solve 31322 of the
puzzles, leaving 5306 resistant."
--
Chris Kuklewicz
-------------- next part --------------
module Main (main) where
import Data.Ix(inRange,range)
import Data.Char(intToDigit,digitToInt)
import Deduce(deduce,lo,hi)
loC = intToDigit lo
hiC = intToDigit hi
unsetC = pred loC
main = do
all <- getContents
let puzzles = zip [1..] (map parseBoard (lines all))
act (i,p) = do p' <- deduce p
return (i,length p,length p')
mapM_ (\ip -> act ip >>= print) puzzles
parseBoard :: String -> [(Int,Int,Int)]
parseBoard s = map toHint justSet
where rcs = [ (r,c) | r <- range (lo,hi), c <- range (lo,hi) ]
isHint vC = inRange (loC,hiC) vC
justSet = filter (isHint . snd) (zip rcs s)
toHint ((r,c),vC) = (r,c,digitToInt vC)
-------------- next part --------------
{- By Chris Kuklewicz <haskell at list.mightyreason.com> -}
module Deduce (deduce,lo,hi) where
{- The exposed function deduce takes a list of (row,column,value)
tuples that are the known parts of the solutions and returns a
(hopefully longer) list in the same format. The indices can be any
enumerated type in the range (lo,hi).
-}
import Data.Array.Diff (assocs,(!),(//),ixmap,range,inRange,accumArray,DiffArray,Ix)
import Data.List(sortBy,groupBy,transpose,(\\))
import Control.Monad(liftM,guard)
default ()
-- Typesafe values for indices
-- This machinery allows for more type safety than if R,C,V,B,D were all Int or Char
type E = Int
newtype R = R E deriving (Eq,Ord,Ix,Enum,Show) -- Row index
newtype C = C E deriving (Eq,Ord,Ix,Enum,Show) -- Column index
newtype V = V E deriving (Eq,Ord,Ix,Enum,Show) -- Value index
newtype B = B E deriving (Eq,Ord,Ix,Enum,Show) -- 3x3 Block index
newtype D = D E deriving (Eq,Ord,Ix,Enum,Show) -- Inside 3x3 Block index
lo,hi :: (Enum a) => a
lo = toEnum 1
hi = toEnum 9
fullRange :: (Enum a,Ix a) => [a]
fullRange = range (lo,hi)
rcToBD (R r) (C c) = let (rq,rm) = quotRem (r-lo) 3
(cq,cm) = quotRem (c-lo) 3
b = lo + ( 3*rq + cq )
d = lo + ( 3*rm + cm )
in (B b, D d)
bdToRC (B b) (D d) = let (bq,bm) = quotRem (b-lo) 3
(dq,dm) = quotRem (d-lo) 3
r = lo + ( 3*bq + dq )
c = lo + ( 3*bm + dm )
in (R r, C c)
-- Typeclasses and Data for "shuffle" and "unshuffle"
class (Show x, Ix x, Enum x, Ord x) => IE x
instance IE R; instance IE C; instance IE V; instance IE B; instance IE D
data Perms a b c = Perms { shuffle' :: (R,C,V) -> (a,b,c)
, unshuffle' :: (a,b,c) -> (R,C,V) }
-- Reify the types "a b c" to a value of type Perms
class (IE a, IE b, IE c) => Perm a b c where perm :: Perms a b c
instance Perm R C V where perm = Perms id id
instance Perm R V C where perm = Perms (\ (r,c,v) -> (r,v,c)) (\ (r,v,c) -> (r,c,v))
instance Perm C V R where perm = Perms (\ (r,c,v) -> (c,v,r)) (\ (c,v,r) -> (r,c,v))
instance Perm C R V where perm = Perms (\ (r,c,v) -> (c,r,v)) (\ (c,r,v) -> (r,c,v))
instance Perm V R C where perm = Perms (\ (r,c,v) -> (v,r,c)) (\ (v,r,c) -> (r,c,v))
instance Perm V C R where perm = Perms (\ (r,c,v) -> (v,c,r)) (\ (v,c,r) -> (r,c,v))
-- Special cases
instance Perm B D V where perm = Perms (\ (r,c,v) -> let (b,d) = rcToBD r c in (b,d,v))
(\ (b,d,v) -> let (r,c) = bdToRC b d in (r,c,v))
instance Perm V B D where perm = Perms (\ (r,c,v) -> let (b,d) = rcToBD r c in (v,b,d))
(\ (v,b,d) -> let (r,c) = bdToRC b d in (r,c,v))
shuffle :: (Perm a b c) => (R,C,V) -> (a,b,c)
shuffle = shuffle' perm
unshuffle :: (Perm a b c) => (a,b,c) -> (R,C,V)
unshuffle = unshuffle' perm
-- Array types, values and functions
type Index = (R,C,V)
type Cell = Bool
on,off :: Cell
on = True -- Means this might be part of the solution
off = False -- Means this cannot be part of the solution
boundsCube :: (Perm a b c) => ((a,b,c),(a,b,c))
boundsCube = ((lo,lo,lo),(hi,hi,hi))
type Cube = DiffArray Index Cell
emptyCube :: Cube
emptyCube = accumArray const on boundsCube []
type View a b c = DiffArray (a,b,c) Cell
{-# INLINE view #-}
view :: (Perm a b c) => Cube -> View a b c
view cube = ixmap boundsCube unshuffle cube
type Hints = [(Index,Cell)]
isOn :: (Perm a b c) => View a b c -> [(a,b,c)]
isOn = map fst . filter snd . assocs
-- The goal is to create functions that turn the current Cube into a
-- list of Hints. These Hints will be purely subtractive: they all
-- turn a Cell from 'on' to 'off'.
type Rule = Cube -> Hints
-- Small utility functions
fst3 (x,_,_) = x; snd3 (_,x,_) = x; thd3 (_,_,x) = x
fst4 (x,_,_,_) = x; snd4 (_,x,_,_) = x; thd4 (_,_,x,_) = x; fth4 (_,_,_,x) = x
by un bi = (\ left right -> (un left) `bi` (un right))
sortWith un = sortBy (by un compare)
groupWith un = groupBy (by un (==))
groupSort un = groupWith un . sortWith un
atLeastOne = not . null
atLeastTwo (_:_:_) = True; atLeastTwo _ = False
exactlyOne [_] = True; exactlyOne _ = False
oneOrTwo [_] = True; oneOrTwo [_,_] = True; oneOrTwo _ = False
{- ruleBlock1 : When operating on Perm V R C:
Given a value V, look along each row and see which blocks that value
may occupy. Find a row R1 for which the value is allowed in exactly
one block B1 (an no other blocks). This occupies [(B1,d)] in
B1. Eliminate V from the other locations in B1.
Given a value V, look along each row and see which blocks that value
may occupy. Find two rows [R1,R2] for which the value only is allowed
in exactly the same two blocks [B1,B2] (and no others). These occupy
[(B1,d11s)] in R1, [(B2,d12s)] in R1, [(B1,d21s)] in R2, and
[(B2,d22s)] in R2. Eliminate V from the other locations in B1 and the
other locations in B2.
Works for R and C reversed, of course.
-}
{- ruleBlock2 : When operating on Perm V R C:
Given a value V, look inside each block and see which rows that value
may occupy. Find a block B1 for which only one row R1 is occupied
(and no other rows). This occupies [(R1,c11s)] in B1. Eliminate V
from all the other c's in row R1.
Given a value V, look inside each block and see which rows that value
may occupy. Find two blocks [B1,B2] for which the value only is allowed
in exactly the same two rows [R1,R2] (and no others). These occupy
[(R1,c11s)] in B1, [(R1,c12s)] in B2, [(R2,c21s)] in B1, and [(R2,c22s)] in
B2 for some C's. Eliminate V from the other rows in R1 and the
other columns in R2.
Works for R and C reversed, of course.
-}
{- ruleBlockP
There is enough similarity between ruleBlock1 and ruleBlock2 to parameterize
over expand and contract.
The V index is special because it is "orthogonal" to the 3x3 blocks.
The line
sVsA2 = map (filter atLeastTwo) $ sVsA -- drop unique ones
is used to prevent propagating solved constraints, as rule4P should
already do this in its k==1 case.
(assocs view) becomes useful hints through a chain of map, sort, group,
filter, and concat operations. The assemble function also takes
care to remove redundant hints by consulting the view.
-}
{-# INLINE ruleBlockP #-}
ruleBlockP :: forall a b c x y . (IE a, IE b, IE c, Perm V x y)
=> ( (V,x,y) -> (V,a,b,c) ) -- "expand"
-> (V -> b -> [c] -> [Index]) -- "contract"
-> View V x y -> Hints
ruleBlockP expand contract view =
let allOn :: [(V,a,b,c)]
allOn = map expand . isOn $ view
sV :: [ [(V,a,b,c)] ]
sV = groupWith fst4 $ allOn -- group by V
sVsA :: [[ [(V,a,b,c)] ]]
sVsA = map (groupSort snd4) $ sV -- group by a
sVsAsB,sVsAsB2 :: [[[ [(V,a,b,c)] ]]]
sVsAsB = map (map (groupSort thd4)) $ sVsA -- group by b
-- These filters are to remove empty and redundantly full possibilities
sVsAsB2 = filter atLeastOne . map (filter oneOrTwo) $ sVsAsB
sVsAsBgB :: [[ ([b],[[(V,a,b,c)]]) ]]
-- The filter is to remove solved parts of the puzzle (punt to ruleSubsetP)
sVsAsBgB = map (map getAllB . filter (atLeastTwo . concat)) $ sVsAsB2
where getAllB :: [[(V,a,b,c)]] -> ([b],[[(V,a,b,c)]])
getAllB vabcss = (map (thd4 . head) vabcss, vabcss) -- length (concat vabcss) >= 2
useful :: [ [([b],[[(V,a,b,c)]])] ]
useful = concatMap (filter exactlySame . groupSort fst) $ sVsAsBgB
where exactlySame :: [([b],[[(V,a,b,c)]])] -> Bool
exactlySame sas@((sbs,_):_) = length sas == length sbs
assemble :: [ ([b],[[(V,a,b,c)]]) ] -> Hints
assemble stuff = [ (rcv,off) | rcv <- ixs, (view ! shuffle rcv) /= off ]
where byVB :: [[(V,a,b,c)]]
byVB = map concat . transpose . map snd $ stuff -- Regroup by identical 'b'
-- byVB = groupSort thd4 . concat . concat . map snd $ stuff -- equivalent
act :: [(V,a,b,c)] -> [Index]
act allVB@((v,_,b,_):_) = contract v b (fullRange \\ map fth4 allVB)
ixs :: [Index]
ixs = concatMap act byVB
in concatMap assemble useful
{-# INLINE ruleBlock1 #-}
ruleBlock1 :: (Perm V x y) => View V x y -> Hints
ruleBlock1 = ruleBlockP expand contract
where expand all@(v,x,y) = let (r,c,_) = unshuffle all
(b,d) = rcToBD r c
in (v,x,b,d)
contract v b ds = map undo ds
where undo d = let (r,c) = bdToRC b d in (r,c,v)
{-# INLINE ruleBlock2 #-}
ruleBlock2 :: (Perm V x y) => View V x y -> Hints
ruleBlock2 = ruleBlockP expand contract
where expand all@(v,x,y) = let (r,c,_) = unshuffle all
(b,_) = rcToBD r c
in (v,b,x,y)
contract v x ys = map undo ys
where undo y = unshuffle (v,x,y)
ruleBlocks :: [Rule]
ruleBlocks = [ (\ known -> ruleBlock1 (view known :: View V R C) )
, (\ known -> ruleBlock1 (view known :: View V C R) )
, (\ known -> ruleBlock2 (view known :: View V R C) )
, (\ known -> ruleBlock2 (view known :: View V C R) )
]
{- Given a list of locations, such as for the 9 columns of a row,
look at the allowed values at each location. Find a subset of k
columns for which the union of their allowed values [V..] has
length k. Then eliminate [V..] from the (9-k) other columns.
This clearly finds a list of N columns each with the same N values
if such a thing exists, so it subsumes rule1P.
This is fully symmetric in R C and V and depends on the constaints in
R and V but not C. So the (View B D V) case also works.
A useful property of this rule is that once there is only one way
to place a value in a row or column or block then it will propagate
that solution to the related contraints. This is the case when
minK is 1.
-}
{-# INLINE ruleSubsetP #-}
ruleSubsetP :: forall a b c.(Perm a b c) => View a b c -> Hints
ruleSubsetP view =
let allOn :: [(a,b,c)]
allOn = isOn $ view
sAsB :: [[ [(a,b,c)] ]]
sAsB = map (groupWith snd3) . groupWith fst3 $ allOn
sAsBgC :: [ [(Int,[c],[(a,b,c)])] ]
sAsBgC = map (sortWith fst3 . map (\ abcs -> (length abcs
,map thd3 abcs
,abcs) ) ) $ sAsB
makeChains :: [(Int,[c],[(a,b,c)])] -> Int -> [(a,[b],[c])]
makeChains input k = filter check . map toChain . subsets k . upToK $ input
where upToK :: [(Int,[c],[(a,b,c)])] -> [(Int,[c],[(a,b,c)])]
upToK = takeWhile ((k>=).fst3)
toChain :: [(Int,[c],[(a,b,c)])] -> (a,[b],[c])
toChain vals = ( fst3 . head . thd3 . head $ vals -- record "a" for easy retrieval later
, map (snd3 . head . thd3) $ vals -- the bs, length [b] == length vals == k by property of subsets k
, combine . map snd3 $ vals) -- union of the cs at each b in bs
check :: (a,[b],[c]) -> Bool
check (_,_,cs) = (k == length cs) -- check that length [c] == k as well
getUseful :: [(Int,[c],[(a,b,c)])] -> [(a,[b],[c])]
getUseful [] = []
getUseful input = concatMap (makeChains input) [minK .. maxK]
-- assertion: (length input) == (length . combine . map snd3 $ input)
where minK,maxK :: Int
minK = fst3 . head $ input
maxK = pred . length $ input
useful :: [(a,[b],[c])]
useful = concatMap getUseful sAsBgC
{- The chains are (a,bs,cs) such that
cs == nub . sort $ [ z | (x,y,z) <- allOn, x==a, y `elem` bs]
and
length bs == length cs.
Thus all the (a,b in bs,c) in the final puzzle have distict c in cs.
For location (a,b' not in bs,c') cannot have c' in cs,
thus if c' is in cs then (a,b',c') should be turned off.
-}
assemble :: (a,[b],[c]) -> Hints
assemble (a,inBs,inCs) = do -- List Monad
b <- fullRange \\ inBs
c <- inCs
let abc :: (a,b,c)
abc = (a,b,c)
guard (view ! abc)
return (unshuffle abc,off)
in concatMap assemble useful
-- All subsets of length 'k', order is stable
subsets :: Int -> [a] -> [[a]]
subsets 0 _ = [[]]
subsets _ [] = []
subsets k (x:xs) = (fmap (x:) (subsets (pred k) xs)) ++ subsets k (xs)
-- Hopefully efficient merge of (list of (sorted lists)), unique values only
combine :: (Ord a) => [[a]] -> [a]
combine [] = []
combine [x] = x
combine xs = let (a,b) = split xs
in merge (combine a) (combine b)
where split [] = ([],[])
split [a] = ([a],[])
split (x:y:cs) = let (a,b) = split cs in (x:a,y:b)
merge a [] = a
merge [] b = b
merge a@(x:a') b@(y:b') = case compare x y of
EQ -> x : merge a' b'
LT -> x : merge a' b
GT -> y : merge a b'
{-# INLINE eachPerm #-}
eachPerm :: (forall x y z. (Perm x y z) => View x y z -> Hints) -> [ Rule ]
eachPerm rule =
[ (\ known -> rule ( view known :: View R C V ) )
, (\ known -> rule ( view known :: View R V C ) )
, (\ known -> rule ( view known :: View C V R ) )
, (\ known -> rule ( view known :: View C R V ) )
, (\ known -> rule ( view known :: View V R C ) )
, (\ known -> rule ( view known :: View V C R ) )
]
ruleSubsets :: [ Rule ]
ruleSubsets = eachPerm ruleSubsetP ++ [ (\ known -> ruleSubsetP (view known :: View B D V) ) ]
allRules :: [ Rule ]
allRules = ruleSubsets ++ ruleBlocks
-- Applying the rules
{- The evolution strategy is simple:
Apply each rule in turn, keeping track whether or not there were any changes.
If all rules cause no change then it is done evolving.
-}
evolve :: Cube -> Cube
evolve cube =
let (cube',changed) = foldl step (cube,False) allRules
in if changed then evolve cube' else cube'
where step orig@(known,_) rule =
case rule known of
[] -> orig
hints -> (known // hints,True)
toCube :: (Monad m,Enum e) => [(e,e,e)] -> m Cube
toCube locs = do hints <- liftM concat $ mapM setLoc locs
return (emptyCube // hints)
where setLoc:: (Enum e,Monad m) => (e,e,e) -> m Hints
setLoc i@(re,ce,ve) = mapM checkM (rs ++ cs ++ vs)
where r = toEnum $ fromEnum re
c = toEnum $ fromEnum ce
v = toEnum $ fromEnum ve
rs = [((r',c,v),off) | r' <- fullRange, r' /= r]
cs = [((r,c',v),off) | c' <- fullRange, c' /= c]
vs = [((r,c,v'),off) | v' <- fullRange, v' /= v]
checkM hint = if check hint then return hint
else fail "Input location is out of range"
check ((r,c,v),_) = and [ inRange (lo,hi) r
, inRange (lo,hi) c
, inRange (lo,hi) v ]
fromCube :: (Enum e) => Cube -> [(e,e,e)]
fromCube cube = map head . filter exactlyOne $
[ [ eee | v <- fullRange, cube ! (r,c,v)
, let eee = (toEnum $ fromEnum r
,toEnum $ fromEnum c
,toEnum $ fromEnum v) ]
| r <- fullRange, c <- fullRange ]
consistent :: Cube -> Bool
consistent known = and [ consistentView ( view known :: View R C V )
, consistentView ( view known :: View C V R )
, consistentView ( view known :: View R V C )
, consistentView ( view known :: View V B D )
]
where
consistentView :: (Perm a b c) => View a b c -> Bool
consistentView view = and [ atLeastOne [ () | c <- fullRange, view ! (a,b,c) ]
| a <- fullRange, b <- fullRange ]
checkCube :: (Monad m) => String -> Cube -> m Cube
checkCube msg cube = if consistent cube then return cube else fail msg
deduce :: (Monad m,Enum e) => [(e,e,e)] -> m [(e,e,e)]
deduce locs = toCube locs >>=
checkCube "Inconsistent locations passed in" >>=
return . evolve >>=
checkCube "Inconsistent cube deduced from input" >>=
return . fromCube
test :: [(E,E,E)]
test = [(1,8,1),(1,9,2),(2,5,3),(2,6,5),(3,4,6),(3,8,7),(4,1,7),(4,7,3),(5,4,4),(5,7,8),(6,1,1),(7,4,1),(7,5,2),(8,2,8),(8,8,4),(9,2,5),(9,7,6)]
testC :: IO Cube
testC = toCube test
check :: IO ()
check = do c <- testC
print (consistent (evolve c))
More information about the Haskell-Cafe
mailing list