#!/usr/bin/perl # # solve [-p 0|1|2] [-d 0|1] [-s [0|1] [-w #] [-l ] [-e ] # # solves sudoku as best I know how. # # option -p 0 -- only print the puzzle at the end [default] # option -p 1 -- print after each pass over the board # option -p 2 -- print after each grouping is considered # option -d 0 -- omit decision information to printout [default] # option -d 1 -- add decision information to printout # option -s 0 -- don't print summary text from each pass # option -s 1 -- print summary text from each pass [default] # option -w # -- how wide to make each cell in the printout [3] # option -l -- how long to wait between puzzle printouts [0.25] # option -e -- solve the given file, can be - to read from stdin # # input file read is fairly flexible. a comma separated list is ideal. #it will accept files in the initializer format used below for $tough #and $diabolical [try cutting and pasteing]. for completness it also #accepts puzzles in the ascii-art box format generated as output from #this program. # # please let me know if there are valid sudoku that this fails to solve. # v0.2 09/19/2005 works good enough for all the puzzles I have # v1.6 10/09/2005 fast, thorough, works. # v1.7 10/10/2005 added file-read # v2.0 11/30/2005 deals with conflicts from implications not just # in same group [row/colum/box] for values with only # two options. [does not work for n-options.] # v2.1 12/29/2005 fixed stupid return value bug that broke some puzzles # v2.2 01/03/2005 actually fixed above bug. [oops] # esw@alum.mit.edu from http://wile.org/sudoku/ $diabolical=[ [1,0,5,0,7,0,4,0,0], [0,0,0,0,0,1,9,5,0], [7,0,8,5,0,0,0,0,0], [2,0,0,0,3,7,0,9,0], [0,0,0,0,2,0,0,0,0], [0,3,0,9,4,0,0,0,5], [0,0,0,0,0,4,5,0,2], [0,7,3,6,0,0,0,0,0], [0,0,9,0,1,0,3,0,4], ]; $tough=[ [0,0,6,0,1,0,9,0,0], [0,0,0,0,0,8,7,4,0], [0,0,0,0,2,0,3,1,0], [4,0,0,0,0,9,0,8,0], [0,0,5,2,0,6,4,0,0], [0,8,0,3,0,0,0,0,2], [0,1,7,0,6,0,0,0,0], [0,3,4,5,0,0,0,0,0], [0,0,9,0,3,0,1,0,0], ]; require 'getopts.pl'; Getopts('p:s:d:w:l:e:'); #actual puzzle solving is done on $usepuzzle $usepuzzle=$tough; if ("$opt_e" ne "") { $usepuzzle=readpuzzlefile($opt_e); } $progress=1; if ("$opt_s" ne "") { $progress=$opt_s; } $printing=0; $pausing=0; if ("$opt_p" ne "") { $printing=$pausing=$opt_p; } $pause=0.25; if ("$opt_l" ne "") { $pause=$opt_l; } $printwidth=3; if ("$opt_d" ne "") { $printwidth=5 } if ("$opt_w" ne "") { $printwidth=$opt_w; } $printopts=0; if ("$opt_d" ne "") { $printopts=$opt_d; } sub readpuzzlefile($) { my($enterfile)=@_; if ("$enterfile" eq "-") { $fh=STDIN; } else { open($fh, "$enterfile") || die "can't open $enterfile\n"; } my($linecount, @mypuz)=(0,()); while(<$fh>) { chomp; my(@line)=split /[\[\],|]/; @line=map { (/^$/)?():$_; } @line; if ($#line == 8) { my(@row) = map { (/^(\W)+$/)?0:$_; } @line; @row=map { (s/(\W)+//g); $_; } @row; $mypuz[$linecount]=\@row; $linecount++; } } if ($linecount == 9) { print "using given puzzle with $#mypuz+1 rows\n" if $progress; return \@mypuz; } else { die "only read $linecount reasonable lines from $enterfile"; } } sub boxidx($$) { my($outer,$inner)=@_; return int($outer/3)*3+int($inner/3),($outer%3)*3+$inner%3; } sub rowidx($$) { return $_[1],$_[0]; } sub colidx($$) { return $_[0],$_[1]; } sub anythingspossible($) { my($solved)=@_; my($opts, $countopts); for( $i=0; $i<9; $i++) { for( $j=0; $j<9; $j++) { $opts->[$i][$j]{'solved'}=0; $opts->[$i][$j]{'i'}=1+$i; $opts->[$i][$j]{'j'}=1+$j; if ($solved->[$i][$j] ne 0) { $opts->[$i][$j]{'freeopt'}=1<<($solved->[$i][$j]-1); $opts->[$i][$j]{'decided'}=$opts->[$i][$j]{'freeopt'}; $countopts++; } else { $opts->[$i][$j]{'freeopt'}=(2**9)-1; # bits 1-9 all set $opts->[$i][$j]{'decided'}=0; $countopts+=9; } } } return ($opts, $countopts); } sub bitset($$) { my($opt, $bit)=@_; return $opt & (1<<($bit)); } sub unset($$) { my($opt, $bit)=@_; return $opt & (~(1<<($bit))); } sub optshash($) { my($opt)=@_; return join('', map { bitset($opt,$_)?$_+1:'' } (0..8)); } sub printpuz($$@) { my($opts,$showopts,$width)=@_; if ($width < 2) { $width=3; } my($hpad)=join("", map { "-" } (1..$width)); my($hsep)=join("*", map { $hpad } (1..9) ); print "*$hsep*\n"; foreach my $optrow (@$opts) { my(@vals)=map { optshash($_->{'freeopt'} | $_->{'solved'} ) } @$optrow; my(@solved)=map { if (length($_) eq 1) { $_ } else { ' ' } } @vals; my(@wideval)=map { sprintf "%-*s", 1+$width/2,$_ } @solved; my($line)=join("|", map { sprintf "%*s", $width, $_ } @wideval); if ($showopts) { $hsep=join("*", map { sprintf "%.*s", $width,$_.$hpad } @vals); } print "|$line|\n"; print "*$hsep*\n"; } select(undef,undef,undef,$pause) if $pausing; } sub numbits($) { my($optcp)=@_; my($optcount)=0; foreach (1..9) { if ($optcp&1) { $optcount++; } $optcp=$optcp>>1; } return $optcount; } sub combine($@) { my($lastcombinationsref, @newcombinations)=@_; my(@combinations); foreach my $outercombination (@$lastcombinationsref) { my($outervalue,@alreadyusedentries)=@$outercombination; foreach my $newcombination (@newcombinations) { my($newvalue,$curentry)=@$newcombination; my($disqualified)=0; foreach my $testentry (@alreadyusedentries) { #only have to check each combination, not permutation if ($testentry le $curentry) { $disqualified=1; } } if (! $disqualified) { my(@usedinentry)=@alreadyusedentries; push @usedinentry, $curentry; push @combinations, [ $outervalue|$newvalue, @usedinentry]; } } } return @combinations; } @checkdecision=[ \&valuefordecisionlist, \&nullvectorinentriesnotinlist, 1 ]; # width==1 is the basic sudoku rules for rows/columns/boxes: # if an entry "claims" exclusively a value [eg it is a # or it can be none other than that value] then disallow # that value for the others in the group # # width==n if amongst n entries, n values are "claimed" exclusively, # then disallow those n values for others in the group. @checkforced=[ \&valueforforcedlist, \&entriestosetforforced, 0 ]; # width==1 if any entry is the only one that could possibly be # a certain value, eliminate the other possible values # that entry might have been # # width==n if amongst n values, n entries the only ones which # could possibly satisfy that value, eliminate other # possible values for each of the n entries. sub check($$$$@) { my($opts, $idxfn, $x, $width, $functions)=@_; ($valueforinit, $entriestoset, $commit)=@$functions; my(@groupoptions)=map { ($i,$j)=&$idxfn($x,$_); \$opts->[$i][$j]{'freeopt'}} (0..8); my(@basecombinations)=map { &$valueforinit($_,\@groupoptions) } (0..8); my(@combinations)=@basecombinations; foreach (2..$width) { # generates all $width-combinations of entries @combinations=combine(\@combinations,@basecombinations); } foreach my $combination (@combinations) { ($entry,@usedinentry)=@$combination; if (numbits($entry) eq $width) { my($newlimit, @entriestoset)=&$entriestoset($entry, @usedinentry); foreach my $optionsref (@groupoptions[@entriestoset]) { $$optionsref &= $newlimit; } } } return (($width eq 1) && $commit); } sub checkinfer($$$$) { my($opts, $idxfn, $x, $ruleref)=@_; foreach $val (0..8) { my(@vallist)=(); foreach $y (0..8) { ($i,$j)=&$idxfn($x,$y); $thisopt=\$opts->[$i][$j]{'freeopt'}; if (bitset($$thisopt, $val)) { my(@tuple)=($val, $i, $j, $$thisopt); push(@vallist, \@tuple); } } foreach $tupleidx (0..$#vallist) { my(@tuple)=@{$vallist[$tupleidx]}; foreach $tuplecompareidx (0..$#vallist) { my(@impliedtuple)=@{$vallist[$tuplecompareidx]}; if ($tupleidx != $tuplecompareidx) { # if this ($val, $i, $j) were true, then what's implied? # i think I have to say "i would be $val if $i,$j was $otherval my($val)=$tuple[0]; my($choicehash)=optshash($tuple[3]); my($remainval)=unset($impliedtuple[3],$val); #only deal with single choice left options... if (numbits($remainval)==1) { my($remainhash)=optshash($remainval); # only one option in remainhash $impliedtuple[0]=$remainhash-1; addtuple($ruleref, \@tuple, \@impliedtuple); # print "we have ", keys %rules, "ref: ", \%rules, " \n"; } } } } } } sub displayrules($) { my($rulesref)=@_; foreach $id ( sort keys %$rulesref ) { my($ourhashref)=$rulesref->{$id}; print "$id -- ", join(", ",map {"$_=$ourhashref->{$_}"} sort keys %$ourhashref), "\n"; } } sub rulesstats($) { my($rulesref)=@_; my($numrules)=0; foreach $id ( sort keys %$rulesref ) { my($ourhashref)=$rulesref->{$id}; my(@thiskeys)=keys %$ourhashref; $numrules+=$#thiskeys; } return $numrules; } #two ways... either self-inconsistencies, or #things that must be true for both (x,y)=b & (x,y)=c where b&c are sub checkrules($$) { my($opts,$rulesref)=@_; my(%newrules); my($numremoved)=0; foreach $id ( sort keys %$rulesref ) { my($ourhashref)=$rulesref->{$id}; %{$newrules{$id}}=%$ourhashref; } foreach $id ( sort keys %$rulesref ) { my($ourhashref)=$rulesref->{$id}; foreach $implcoord ( keys %$ourhashref ) { my($implication)="$implcoord==$ourhashref->{$implcoord}"; my($impliedhashref)=$rulesref->{$implication}; foreach $secondimplcoord ( keys %$impliedhashref) { my($implvalfororig)=$newrules{$id}->{$secondimplcoord}; my($secondimplval)=$impliedhashref->{$secondimplcoord}; my($res)=addid(\%newrules, $id, $secondimplcoord, $secondimplval); if ($res == 1) { $id =~ /\((\d+),(\d+)\)==(\d+)/; my($i,$j,$val)=($1,$2,$3); my($optref)=\$opts->[$i][$j]{'freeopt'}; if (bitset($$optref, $val)) { print "$i,$j can't be $val because $secondimplcoord has to be $secondimplval and $implvalfororig\n" if ($printing == 3); #remove $val as an option in $opts[$i][$j] $$optref=unset($$optref, $val); $numremoved++; } # else it's already been removed. } # print "adding $secondimplication to $id\n"; } } } foreach $id ( sort keys %newrules ) { my($ourhashref)=$newrules{$id}; %{$rulesref->{$id}}=%$ourhashref; } return $numremoved; } sub addtuple($$$) { my($rulesref, $tupleref, $tuple2ref)=@_; my($id)="($tupleref->[1],$tupleref->[2])==$tupleref->[0]"; my($impliedidx)="($tuple2ref->[1],$tuple2ref->[2])"; my($impliedval)=$tuple2ref->[0]; return addid($rulesref, $id, $impliedidx, $impliedval); } sub addid($$$$) { my($rulesref, $id, $impliedidx, $impliedval)=@_; my($implied)="$impliedidx==$impliedval"; return 0 if ("$implied" eq "$id"); my($hashref)=$rulesref->{$id}; foreach $idx (keys %$hashref) { return 0 if ("$implied" eq "$idx==$hashref->{$idx}"); } #contradiction! return 1 if ( "$rulesref->{$id}->{$impliedidx}" ne "" ); $rulesref->{$id}->{$impliedidx}=$impliedval; return 0 } #these next two functions are for the checkalreadydecided type of #checking: if the entry or entries claim a value, then disallow for #the rest of the group. sub valuefordecisionlist($$) { my($value,$entries)=@_; my($entryref)=$entries->[$value]; if (! $$entryref) { return (); } return [$$entryref, $value] ; } sub setvectorinentriesnotinlist($@) { my($valuevector, @entriesused)=@_; my($val,@other)=nullvectorinentriesnotinlist($valuevector, @entriesused); return (~$val,@other); } #make sure those bits are unset in all the other entries! sub nullvectorinentriesnotinlist($@) { my($valuevector, @entriesused)=@_; my(@omit)=sort { $b <=> $a } @entriesused; #should be already sorted, but just checking to be safe. my(@otherentries); my($topomit)=pop(@omit); foreach my $val (0..8) { if ( ($#omit > -1) && ( $val > $topomit)) { $topomit=pop(@omit); } if ( $val != $topomit) { push @otherentries, $val; } } return (~$valuevector, @otherentries); } #these next two funcitions are for the checkforced type of checking: #iterating over each value in a group and checking if one [or more] #entries are the only ones to possibly satisfy the requirement. #values are stored packed in the bits of each entry, grab a slice sub valueforforcedlist($$) { my($value,$entries)=@_; my($accum)=0; foreach my $entry (@$entries) { $accum = ($accum<<1) | ($$entry>>$value & 1); } if (! $accum ) { return (); } return [$accum, $value]; } #the values in @usedvalues are turned into a bit-vector mask. this mask #is applied to the options for each entry used in the winning combination, #in order to force that entry to take on this value. [or n-entries,n-values] sub entriestosetforforced($@) { my($entryvector, @usedvalues)=@_; my($newlimit)=0; foreach my $value (@usedvalues) { $newlimit |= 1<<$value; } my(@forcelist)=(); foreach my $idx (0..8) { if (($entryvector>>(8-$idx))&1) { push @forcelist, $idx; } } return ( $newlimit, @forcelist ); } #mostly just an optimization -- the transition from an undecided cell #which is a bit-vector in 'freeopt' to a 'decided' cell happens asap, #once it has gone through a checkdecision-type checking round #to propagate its implications, then its bit vector is retired [set #to zero] and it is 'solved'. zero'd bit vectors speed things up #because they get excluded from all combinations. sub markdecided($$) { my($opts,$commitsolved)=@_; my($solvedcount, $decidedcount, $countopts )=(0,0,0); foreach my $optrow (@$opts) { foreach my $opt (@$optrow) { my($numbits)=numbits($opt->{'freeopt'}|$opt->{'solved'}); $countopts+=$numbits; if ($numbits eq 0) { printpuz($opts,1,$printwidth); die "puzzle error: square $opt->{'i'},$opt->{'j'} with no options available!!!"; } if ( $opt->{'decided'} && $commitsolved) { $opt->{'solved'}=$opt->{'decided'}; $opt->{'decided'}=0; $opt->{'freeopt'}=0; $solvedcount++; } if ( !$opt->{'solved'} && !$opt->{'decided'} && ($numbits eq 1)) { $opt->{'decided'}=$opt->{'freeopt'}; $decidedcount++; } } } return ( $solvedcount, $decidedcount, $countopts ); } sub solve($) { my($solved)=@_; my($lastopts,$commitflag,$committed,$decided); my(%rulebase); my($opts, $numopts)=anythingspossible($solved); print "initially have $numopts options\n" if $progress; my($width)=1; printpuz($opts,$printopts,$printwidth) if $printing; @idxfns= ( \&rowidx, \&colidx, \&boxidx ); @checkfnrefs= ( \@checkdecision, \@checkforced ); while(($width < 9) && ($numopts > 81)) { $lastopts=$numopts; if ($width == 3) { %rulebase={}; foreach my $idxfn (@idxfns) { foreach my $x (0..8) { checkinfer($opts,$idxfn,$x, \%rulebase); } } my($numremoved,$lastnumrules)=0; my($numrules)=rulesstats(\%rulebase); my($depth)=0; while (($numremoved == 0) && ($numrules != $lastnumrules)) { $lastnumrules=$numrules; $numremoved=checkrules($opts, \%rulebase); $numrules=rulesstats(\%rulebase); $depth++; } ($committed,$decided,$numopts)=markdecided($opts,0); print "now have $numopts options at $width/2 $decided decided, $committed committed, $numrules numrules, $depth depth\n" if $progress; if ($numremoved != 0) { $width=1; printpuz($opts,$printopts,$printwidth) if $printing; } } foreach my $checktype (@checkfnrefs) { foreach my $idxfn (@idxfns) { foreach my $x (0..8) { $commitflag=check($opts,$idxfn,$x,$width,@{$checktype}); printpuz($opts,$printopts,$printwidth) if ($printing==2); } } ($committed,$decided,$numopts)=markdecided($opts,$commitflag); print "now have $numopts options at $width/${$checktype}[0][2] $decided decided, $committed committed\n" if $progress; } if ($numopts == $lastopts) { $width++; # no luck? -- try looking for 2 or 3 at-a-time, etc. } else { $width=1; # stay at one-at-a-time while that works ok. } printpuz($opts,$printopts,$printwidth) if $printing; } printpuz($opts,$printopts,$printwidth) if !$printing; if ($numopts != 81) { print "unsolveable by this program. [no unique solution?]\n"; } } solve($usepuzzle);