@@ -109,7 +109,8 @@ def sat(x: List[int], length=13, s="Dynamic programming solves this puzzle!!!"):
109109 return all (s [x [i ]] <= s [x [i + 1 ]] and x [i + 1 ] > x [i ] >= 0 for i in range (length - 1 ))
110110
111111 @staticmethod
112- def sol (length , s ): # O(N^2) method. Todo: add binary search solution which is O(n log n)
112+ def sol (length , s ):
113+ # O(N^2) method. Todo: add binary search solution which is O(n log n)
113114 if s == "" :
114115 return []
115116 n = len (s )
@@ -146,7 +147,8 @@ def sat(x: List[int], length=20, s="Dynamic programming solves this classic job-
146147 return all (s [x [i ]] <= s [x [i + 1 ]] and x [i + 1 ] > x [i ] for i in range (length - 1 ))
147148
148149 @staticmethod
149- def sol (length , s ): # O(N^2) method. Todo: add binary search solution which is O(n log n)
150+ def sol (length , s ):
151+ # O(N^2) method. Todo: add binary search solution which is O(n log n)
150152 if s == "" :
151153 return []
152154 n = len (s )
@@ -442,6 +444,231 @@ def gen_random(self):
442444 self .add (dict (target = target , max_stamps = max_stamps , options = options ))
443445
444446
447+ class Sudoku (PuzzleGenerator ):
448+ """The classic game of [Sudoku](https://en.wikipedia.org/wiki/Sudoku)"""
449+
450+ @staticmethod
451+ def sat (x : str , puz = '____9_2___7__________1_8_4____2_78____4_____1____69____2_8___5__6__3_7___49______' ):
452+ """Find the unique valid solution to the Sudoku puzzle"""
453+ assert all (c == "_" or c == s for (c , s ) in zip (puz , x ))
454+
455+ full = set ('123456789' )
456+ for i in range (9 ):
457+ assert {x [i ] for i in range (9 * i , 9 * i + 9 )} == full , "invalid row"
458+ assert {x [i ] for i in range (i , i + 81 , 9 )} == full , "invalid column"
459+ assert {x [9 * a + b + i + 26 * (i % 3 )] for a in range (3 ) for b in range (3 )} == full , "invalid square"
460+
461+ return True
462+
463+ @staticmethod
464+ def solve (puz ):
465+ """Simple depth-first backtracking solver that branches at the square with fewest possibilities"""
466+ sets = [{int (c )} if c != '_' else set (range (1 , 10 )) for c in puz ]
467+
468+ groups = []
469+ for i in range (9 ):
470+ groups .append (list (range (9 * i , 9 * i + 9 )))
471+ groups .append (list (range (i , i + 81 , 9 )))
472+ groups .append ([9 * a + b + i + 26 * (i % 3 ) for a in range (3 ) for b in range (3 )])
473+
474+ inv = [[] for i in range (81 )]
475+ for g in groups :
476+ for i in g :
477+ inv [i ].append (g )
478+
479+ def reduce ():
480+ """Reduce possibilities and return False if it's clearly impossible to solve, True otherwise.
481+ Repeatedly applies two types of logic:
482+ * When an entry has a single possibility, remove that value from all 20 neighbors
483+ * When a row/col/square has only one entry with k as a possibility, fill in that possibility
484+ """
485+ done = False
486+ while not done :
487+ done = True
488+ for i in range (81 ):
489+ new = sets [i ] - {k for g in inv [i ] for j in g if j != i and len (sets [j ]) == 1 for k in sets [j ]}
490+ if not new :
491+ return False
492+ if len (sets [i ]) != len (new ):
493+ sets [i ] = new
494+ done = False
495+
496+ for g in groups :
497+ for k in range (1 , 10 ):
498+ possibilities = [i for i in g if k in sets [i ]]
499+ if not possibilities :
500+ return False
501+ if len (possibilities ) == 1 :
502+ i = possibilities [0 ]
503+ if len (sets [i ]) > 1 :
504+ done = False
505+ sets [i ] = {k }
506+
507+ return True
508+
509+ ans = []
510+
511+ counter = 0
512+
513+ def solve_helper ():
514+ nonlocal sets , ans , counter
515+ counter += 1
516+ assert len (ans ) <= 1 , "Sudoku puzzle should have a unique solution"
517+ old_sets = sets [:]
518+ if reduce ():
519+ if all (len (s ) == 1 for s in sets ):
520+ ans .append ("" .join (str (list (s )[0 ]) for s in sets ))
521+ else :
522+ smallest_set = min (range (81 ), key = lambda i : len (sets [i ]) if len (sets [i ]) > 1 else 10 )
523+ for v in sorted (sets [smallest_set ]):
524+ sets [smallest_set ] = {v }
525+ solve_helper ()
526+
527+ sets = old_sets
528+
529+ solve_helper ()
530+ assert ans , "No solution found"
531+ return ans [0 ]
532+
533+ @staticmethod
534+ def print_board (board ):
535+ """Helpful method used for development"""
536+ for i in range (9 ):
537+ for j in range (9 ):
538+ print (board [9 * i + j ], end = " " if j == 2 or j == 5 else "" )
539+ print ()
540+ if i == 2 or i == 5 :
541+ print ()
542+
543+ @staticmethod
544+ def print_sets (sets ):
545+ """Helpful method used for development"""
546+ ans = ""
547+ for i in range (9 ):
548+ for j in range (9 ):
549+ ans += " " + "" .join (str (k ) if k in sets [9 * i + j ] else "_" for k in range (1 , 10 ))
550+ if j == 2 or j == 5 :
551+ ans += " | "
552+ if i == 8 :
553+ print (ans )
554+ return
555+ ans += "\n "
556+ if i == 2 or i == 5 :
557+ ans += "\n "
558+
559+
560+ @staticmethod
561+ def gen_sudoku_puzzle (rand ):
562+
563+ groups = []
564+ for i in range (9 ):
565+ groups .append (list (range (9 * i , 9 * i + 9 )))
566+ groups .append (list (range (i , i + 81 , 9 )))
567+ groups .append ([9 * a + b + i + 26 * (i % 3 ) for a in range (3 ) for b in range (3 )])
568+
569+ inv = [[] for i in range (81 )]
570+ for g in groups :
571+ for i in g :
572+ inv [i ].append (g )
573+
574+ def solve (puz ):
575+ """Basically the same as our solver above except that it returns a list of (up to 2) solutions."""
576+ sets = [{int (c )} if c != '_' else set (range (1 , 10 )) for c in puz ]
577+
578+ def reduce ():
579+ """Reduce possibilities and return False if it's clearly impossible to solve, True otherwise.
580+ Repeatedly applies two types of logic:
581+ * When an entry has a single possibility, remove that value from all 20 neighbors
582+ * When a row/col/square has only one entry with k as a possibility, fill in that possibility
583+ """
584+ done = False
585+ while not done :
586+ done = True
587+ for i in range (81 ):
588+ new = sets [i ] - {k for g in inv [i ] for j in g if j != i and len (sets [j ]) == 1 for k in sets [j ]}
589+ if not new :
590+ return False
591+ if len (sets [i ]) != len (new ):
592+ sets [i ] = new
593+ done = False
594+
595+ for g in groups :
596+ for k in range (1 , 10 ):
597+ possibilities = [i for i in g if k in sets [i ]]
598+ if not possibilities :
599+ return False
600+ if len (possibilities ) == 1 :
601+ i = possibilities [0 ]
602+ if len (sets [i ]) > 1 :
603+ done = False
604+ sets [i ] = {k }
605+
606+ return True
607+
608+ ans = []
609+
610+ counter = 0
611+
612+ def solve_helper ():
613+ nonlocal sets , ans , counter
614+ counter += 1
615+ if len (ans ) > 1 :
616+ return
617+ old_sets = sets [:]
618+ if reduce ():
619+ if all (len (s ) == 1 for s in sets ):
620+ ans .append ("" .join (str (list (s )[0 ]) for s in sets ))
621+ else :
622+ smallest_set = min (range (81 ), key = lambda i : len (sets [i ]) if len (sets [i ]) > 1 else 10 )
623+ pi = sorted (sets [smallest_set ])
624+ rand .shuffle (pi )
625+ for v in pi :
626+ sets [smallest_set ] = {v }
627+ solve_helper ()
628+
629+ sets = old_sets
630+
631+ solve_helper ()
632+ return ans
633+
634+ x = ["_" ] * 81
635+ perm = list ("123456789" )
636+ rand .shuffle (perm )
637+ x [:9 ] == perm
638+ x = list (solve (x )[0 ])
639+
640+ done = False
641+ while not done :
642+ done = True
643+ pi = list ([i for i in range (81 ) if x [i ]!= "_" ])
644+ rand .shuffle (pi )
645+ for i in pi :
646+ old = x [i ]
647+ x [i ] = "_"
648+ ans = solve ("" .join (x ))
649+ assert ans
650+ if len (ans )> 1 :
651+ x [i ] = old
652+ else :
653+ done = False
654+ # print()
655+ # Sudoku.print_board(x)
656+ # print(" ", 81-x.count("_"))
657+
658+ return "" .join (x )
659+
660+
661+ def gen_random (self ):
662+
663+ puz = None
664+ for attempt in range (10 if len (self .instances )< 10 else 1 ):
665+ puz2 = Sudoku .gen_sudoku_puzzle (self .random )
666+ if puz is None or puz2 .count ("_" ) > puz .count ("_" ):
667+ puz = puz2
668+
669+ self .add (dict (puz = puz ))
670+
671+
445672class SquaringTheSquare (PuzzleGenerator ):
446673 """[Squaring the square](https://en.wikipedia.org/wiki/Squaring_the_square)
447674 Wikipedia gives a minimal [solution with 21 squares](https://en.wikipedia.org/wiki/Squaring_the_square)
0 commit comments