@@ -109,7 +109,8 @@ def sat(x: List[int], length=13, s="Dynamic programming solves this puzzle!!!"):
109
109
return all (s [x [i ]] <= s [x [i + 1 ]] and x [i + 1 ] > x [i ] >= 0 for i in range (length - 1 ))
110
110
111
111
@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)
113
114
if s == "" :
114
115
return []
115
116
n = len (s )
@@ -146,7 +147,8 @@ def sat(x: List[int], length=20, s="Dynamic programming solves this classic job-
146
147
return all (s [x [i ]] <= s [x [i + 1 ]] and x [i + 1 ] > x [i ] for i in range (length - 1 ))
147
148
148
149
@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)
150
152
if s == "" :
151
153
return []
152
154
n = len (s )
@@ -442,6 +444,231 @@ def gen_random(self):
442
444
self .add (dict (target = target , max_stamps = max_stamps , options = options ))
443
445
444
446
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
+
445
672
class SquaringTheSquare (PuzzleGenerator ):
446
673
"""[Squaring the square](https://en.wikipedia.org/wiki/Squaring_the_square)
447
674
Wikipedia gives a minimal [solution with 21 squares](https://en.wikipedia.org/wiki/Squaring_the_square)
0 commit comments