Sudoku solver, ported from Python

You can post your code snippets here for others to use and learn from
Post Reply
Flinx
Posts: 192
Joined: Sun Feb 14, 2021 9:54 am
Location: Germany

Sudoku solver, ported from Python

Post by Flinx »

A few days ago, Heise online had an article from the magazine c't, which presented a Sudoku solver and generator written in Python.
Because I was interested in the algorithm and wondered if this object-oriented code with the fancy yield command could also be programmed procedurally, I tried to recreate it in Hollywood. The Sudoku solver works, and that's good enough for me. The Sudoku generator shouldn't be as problematic, since the solve() function might have been the hardest to port. I replaced the yield command of the Python program by putting the results into a list, and because a Python generator continues to work with the current values from the point where the yield is triggered, I use these as arguments when recursively calling the function.
So if anyone is interested, here is the script to solve a Sudoku.
The (very good) German description is unfortunately behind a paywall for c't customers, the original Python code (GPL 3) is here.

Code: Select all

columns=8 ; (9-1, index beginnt mit 0)
rows=8
Function p_Reset()
	Local i,j
	board=Nil
	board=CreateList()
	For j=0 To columns
		board[j]=CreateList()
		For i=0 To rows
			board[j][i]=0
		Next
	Next
	results=Nil
	results=CreateList() ; Liste zum Sammeln der Ergebnisse
EndFunction

Function p_print(pboard)
	Local i,j
	For j=0 To columns
		Local output$
		For i=0 To rows
			output$=output$.." "..ToString(pboard[j][i])
		Next
		DebugPrint(ReplaceStr(output$, "0", "."))
	Next
	DebugPrint("")
EndFunction

;prüft, ob eine Zahl num bereits in einer Reihe row oder Spalte column steht:
Function p_number_is_valid(row, column, num)
	For i=0 To 8
	    If board[row][i]=num Or board[i][column]=num Then Return(False)
	Next
	start_column = column\3*3
	start_row = row\3*3
	For i=0 To 2
		For j=0 To 2
			If board[i + start_row][j + start_column] = num Then Return(False)
		Next
	Next
	Return(True)
EndFunction

Function p_solve(r0,c0)
	reclevel=reclevel+1
	Local r,c,n
	; suche ein leeres Feld
	For Local r=r0 To 8
		For Local c=c0 To 8
			If board[r][c] = 0
				; wenn frei
				For Local n=1 To 9
					If p_number_is_valid(r, c, n)
						; wenn die Zahl hier paßt, einsetzen und
						; rekursiv nachsehen, ob das zur Lösung führt
						board[r][c] = n
						p_solve(r,c)
						board[r][c] = 0 ; wieder freigeben und mit nächster 
						; Ziffer oder neuer Position weiter im Suchbaum
					EndIf
				Next
				reclevel=reclevel-1
				Return(False)
			EndIf
		Next
		c0=0
	Next
	; hier angekommen, sind alle Felder ausgefüllt und eine Lösung ist gefunden
	; Lösung in die Liste eintragen
	InsertItem(results, {})
	results[ListItems(results)-1] = CopyTable(board)
	reclevel=reclevel-1
	Return(True)
EndFunction

;--------------------------------------------------

p_Reset()

; zu lösendes Beispiel, es gibt 4 Lösungen
board = {{0, 7, 6, 0, 1, 3, 0, 0, 0},
	 {0, 4, 0, 0, 0, 0, 0, 0, 0},
	 {0, 0, 8, 6, 9, 0, 7, 0, 0},
	 {0, 5, 0, 0, 6, 9, 0, 3, 0},
	 {0, 0, 0, 0, 0, 0, 5, 4, 0},
	 {0, 8, 0, 7, 3, 0, 0, 0, 0},
	 {5, 1, 0, 0, 2, 6, 8, 0, 0},
	 {0, 0, 7, 1, 0, 0, 9, 0, 0},
	 {0, 0, 0, 0, 4, 0, 0, 6, 0}}

p_solve(0,0)
suffix="en"
If ListItems(results)=1 Then suffix=""
DebugPrint("Es gibt",ListItems(results),"Lösung"..suffix..":\n")
For i=0 To ListItems(results)-1
	DebugPrint("Lösung",i+1)
	p_print(results[i])
Next
jalih
Posts: 276
Joined: Fri Jun 18, 2010 8:08 pm
Location: Finland

Re: Sudoku solver, ported from Python

Post by jalih »

Have you tested performance? I wrote a simple Sudoku solver for the 8th programming language. I used bit presentation, so I could use bitwise operations and some bit tricks. It could be made faster by finding singles or hidden singles instead of just selecting empty cell but that would need some extra book keeping.

Code: Select all

\
\  Simple backtracking Sudoku solver for the 8th programming language
\

\ Sub-board window for the given board index
[ 00, 00, 00, 01, 01, 01, 02, 02, 02,
  00, 00, 00, 01, 01, 01, 02, 02, 02,
  00, 00, 00, 01, 01, 01, 02, 02, 02,
  03, 03, 03, 04, 04, 04, 05, 05, 05,
  03, 03, 03, 04, 04, 04, 05, 05, 05,
  03, 03, 03, 04, 04, 04, 05, 05, 05,
  06, 06, 06, 07, 07, 07, 08, 08, 08,
  06, 06, 06, 07, 07, 07, 08, 08, 08,
  06, 06, 06, 07, 07, 07, 08, 08, 08 
] ( swap a:_@ ) curry: window?  \ n -- n

\ Sub-board indices for the given window
[
  [00,01,02,09,10,11,18,19,20],
  [03,04,05,12,13,14,21,22,23],
  [06,07,08,15,16,17,24,25,26],
  [27,28,29,36,37,38,45,46,47],
  [30,31,32,39,40,41,48,49,50],
  [33,34,35,42,43,44,51,52,53],
  [54,55,56,63,64,65,72,73,74],
  [57,58,59,66,67,68,75,76,77],
  [60,61,62,69,70,71,78,79,80]  
] ( swap a:_@ a:_@ ) curry: sub?  \ a n -- a

[
  [0,1,2,3,4,5,6,7,8],
  [9,10,11,12,13,14,15,16,17],
  [18,19,20,21,22,23,24,25,26],
  [27,28,29,30,31,32,33,34,35],
  [36,37,38,39,40,41,42,43,44],
  [45,46,47,48,49,50,51,52,53],
  [54,55,56,57,58,59,60,61,62],
  [63,64,65,66,67,68,69,70,71],
  [72,73,74,75,76,77,78,79,80]
] ( swap a:_@ a:_@ ) curry: row?  \ a n -- a

[
  [0,9,18,27,36,45,54,63],
  [1,10,19,28,37,46,55,64,73],
  [2,11,20,29,38,47,56,65,74],
  [3,12,21,30,39,48,57,66,75],
  [4,13,22,31,40,49,58,67,76],
  [5,14,23,32,41,50,59,68,77],
  [6,15,24,33,42,51,60,69,78],
  [7,16,25,34,43,52,61,70,79],
  [8,17,26,35,44,53,62,71,80]
] ( swap a:_@ a:_@ ) curry: col?  \ a n -- a

: trailing-zero-bits  \ n -- n
  32 >r
  dup n:neg n:band
  dup if -1 n:r+ then
  dup x0000ffff n:band if -16 n:r+ then
  dup x00ff00ff n:band if -8 n:r+ then
  dup x0f0f0f0f n:band if -4 n:r+ then
  dup x33333333 n:band if -2 n:r+ then
  x55555555 n:band if -1 n:r+ then
  r> ;

\ Bit number presentations
a:new 0 a:push ( 1 swap n:shl a:push ) 0 8 loop
( swap a:_@ ) curry: posbit?

: search  \ n -- n n | n null
  dup trailing-zero-bits dup 8 n:> if
    drop null
  then ;

: b-xor  \ n n -- n
  n:bxor 511 n:band ;

: b-not  \ n n -- n
  n:bnot 511 n:band ;

: b-any  \ a -- n
  ' n:bor 0 a:reduce ;

a:new 0 args "Give Sudoku text file as param" thrownull
f:slurp "Cannot read file" thrownull >s "\n" "" s:replace "" s:/
' >n a:map ( posbit? "Bad data" thrownull a:push ) a:each! drop constant board

: display-board
  board ( search nip -1 ?: n:1+ ) a:map
  "+-----+-----+-----+\n"
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "+-----+-----+-----+\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "+-----+-----+-----+\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "|%d %d %d|%d %d %d|%d %d %d|\n" s:+
  "+-----+-----+-----+\n" s:+
  s:strfmt . ;

\ Store move history
a:new constant history

\ Possible numbers for a cell
: candidates?  \ n -- n
  dup dup 9 n:/ n:int swap 9 n:mod \ row col
  board swap col? b-any
  board rot row? b-any
  n:bor
  board rot window? sub? b-any
  n:bor
  b-not ;

\ If found:     -- n T
\ If not found: -- F
: find-free-cell
  false board
  ( !if
      nip true break
    else
      drop
    then ) a:each drop ;

: validate \ -- T
  true
  board
  ( dup -rot a:@ swap 2 pick 0 a:! 2 pick candidates? 2 pick n:= if
      -rot a:!
    else
      3drop
      false swap
      break
    then ) 0 80 loop drop ;

: solve  \ -- T
  repeat
    find-free-cell if
      dup candidates?
      repeat
        search null? if
          drop board -rot a:! drop
          history a:len !if
            drop false ;;
          then
          a:pop nip
          a:open
        else
          n:1+ posbit? dup 
          board 4 pick rot a:! drop
          b-xor
          2 a:close
          history swap a:push drop
          break
        then
      again
    else
      validate break
    then
  again ;

: app:main
  "Sudoku puzzle:\n" .
  display-board cr
  solve if
    "Sudoku solved:\n" .
    display-board
  else
    "No solution!\n" .
  then ;
Solves most Sudokus instantly but here is a test run with "the world hardest Sudoku" that takes a little more time:

Code: Select all

root@DietPi:~# time /opt/8th/bin/rpi64/8th sudoku.8th hard.txt
Sudoku puzzle:
+-----+-----+-----+
|8 0 0|0 0 0|0 0 0|
|0 0 3|6 0 0|0 0 0|
|0 7 0|0 9 0|2 0 0|
+-----+-----+-----+
|0 5 0|0 0 7|0 0 0|
|0 0 0|0 4 5|7 0 0|
|0 0 0|1 0 0|0 3 0|
+-----+-----+-----+
|0 0 1|0 0 0|0 6 8|
|0 0 8|5 0 0|0 1 0|
|0 9 0|0 0 0|4 0 0|
+-----+-----+-----+

Sudoku solved:
+-----+-----+-----+
|8 1 2|7 5 3|6 4 9|
|9 4 3|6 8 2|1 7 5|
|6 7 5|4 9 1|2 8 3|
+-----+-----+-----+
|1 5 4|2 3 7|8 9 6|
|3 6 9|8 4 5|7 2 1|
|2 8 7|1 6 9|5 3 4|
+-----+-----+-----+
|5 2 1|9 7 4|3 6 8|
|4 3 8|5 2 6|9 1 7|
|7 9 6|3 1 8|4 5 2|
+-----+-----+-----+

real	0m0.686s
user	0m0.670s
sys	0m0.014s
root@DietPi:~#  
jalih
Posts: 276
Joined: Fri Jun 18, 2010 8:08 pm
Location: Finland

Re: Sudoku solver, ported from Python

Post by jalih »

I tested the Python version you linked with "the world hardest Sudoku" and result was:

Code: Select all

root@DietPi:~# time python3 sudo.py
Difficulty: Hard

8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2
Solutions:  1

real	1m27.243s
user	1m26.440s
sys	0m0.125s
root@DietPi:~# 
It shows that just using a bit presentation for numbers can give a huge speed boost almost for free!
Flinx
Posts: 192
Joined: Sun Feb 14, 2021 9:54 am
Location: Germany

Re: Sudoku solver, ported from Python

Post by Flinx »

It seems to me that you are missing the point here.
The Python article is about programming style and finding algorithms. The Sudoku solver is just one part, which is needed for the Sudoku generator.
If it was about speed, the author would certainly not have chosen an interpreter language.
We are in the Hollywood forum here, so it doesn't seem reasonable to me to compare Python with 8th.
The Python solver finds all solutions of a Sudoku. Your code stops at the first solution, if I'm seeing this correctly. (And I don't find that 8th would be easier to read).
If I wanted to build a fast sudoku solver, I would try to keep the whole sudoku in processor registers. With 324 bits this is easily possible with a 64 bit CPU. But for this a suitable programming language is necessary, with C this can be done for example by the register declaration (if your compiler still supports it).
Nevertheless I see of course that you have put a lot of effort into your program.
jalih
Posts: 276
Joined: Fri Jun 18, 2010 8:08 pm
Location: Finland

Re: Sudoku solver, ported from Python

Post by jalih »

Flinx wrote: Mon Apr 17, 2023 11:05 am It seems to me that you are missing the point here.
The Python article is about programming style and finding algorithms. The Sudoku solver is just one part, which is needed for the Sudoku generator.
If it was about speed, the author would certainly not have chosen an interpreter language.
We are in the Hollywood forum here, so it doesn't seem reasonable to me to compare Python with 8th.
The Python solver finds all solutions of a Sudoku. Your code stops at the first solution, if I'm seeing this correctly. (And I don't find that 8th would be easier to read).
I didn't miss the point, it seems wasteful to always recursively test and try every possible number candidate 1-9 for empty Sudoku grid cell!

When using bit presentation for numbers, you get a bitmap for empty grid cell with bits set for every possible number candidate for that cell! you can just get the first non-zero bit with binary trick, mask that bit off from bitmap and save grid index and bitmap into history stack. Now, when backtracking the next possible candidate for that cell is directly available in bitmap!

I have seen faster Python Sudoku solvers than my 8th version! Good strategy for selecting next empty grid cell would give the biggest performance improvements as it would limit the search space a lot! Going for singles or hidden singles probably would work well...

You are right, my solver only finds the first valid filled Sudoku but it could easily be modified to find all the possible solutions (just save the copies of valid solutions and backtrack until there are no more possible candidates inside the history stack).
Post Reply