Skip to content

Commit 54f6d97

Browse files
committed
401 probs, finished human_eval puzzles, added sudoku and simplified format
1 parent 83267d8 commit 54f6d97

19 files changed

+18693
-38837
lines changed

generators/ICPC.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ def sat(perms: List[List[int]],
3636
return all(heights0[i] > heights1[j] for i, j in zip(perm0, perm1))
3737

3838
@staticmethod
39-
def sol(prices0=[7, 7, 9, 5, 3, 7, 1, 2],
40-
prices1=[5, 5, 5, 4, 2, 5, 1, 1],
41-
heights0=[2, 4, 9, 3, 8, 5, 5, 4],
42-
heights1=[1, 3, 8, 1, 5, 4, 4, 2]):
39+
def sol(prices0, prices1, heights0, heights1):
4340
n = len(prices0)
4441
prices = [prices0, prices1]
4542
orders = [sorted(range(n), key=lambda i: (prices0[i], heights0[i])),
@@ -116,7 +113,8 @@ def sat(indices: List[int],
116113

117114
# adapted from https://github.com/SnapDragon64/ACMFinalsSolutions/blob/master/finals2019/beautifulbridgesDK.cc
118115
@staticmethod
119-
def sol(H, alpha, beta, xs, ys, thresh): # thresh is ignored
116+
def sol(H, alpha, beta, xs, ys, thresh):
117+
# thresh is ignored
120118
n = len(xs)
121119
cost = [-1] * n
122120
prior = [n] * n

generators/IMO.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ def sat(keep: List[bool], heights=[10, 2, 14, 1, 8, 19, 16, 6, 12, 3, 17, 0, 9,
258258
return all(abs(pi[2 * i] - pi[2 * i + 1]) == 1 for i in range(n))
259259

260260
@staticmethod
261-
def sol(heights): # Based on the judge's solution.
261+
def sol(heights):
262+
# Based on the judge's solution.
262263
n = int(len(heights) ** 0.5)
263264
assert sorted(heights) == list(range(n * (n + 1)))
264265
groups = [h // (n + 1) for h in heights]

generators/basic.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,30 @@ def gen_random(self):
477477
self.add(dict(string=string, count=count))
478478

479479

480+
class CompleteParens(PuzzleGenerator):
481+
@staticmethod
482+
def sat(t: str, s="))(Add)some))parens()to()(balance(()(()(me!)(((("):
483+
"""Add parentheses to the beginning and end of s to make all parentheses balanced"""
484+
for i in range(len(t) + 1):
485+
depth = t[:i].count("(") - t[:i].count(")")
486+
assert depth >= 0
487+
return depth == 0 and s in t
488+
489+
@staticmethod
490+
def sol(s):
491+
return "(" * s.count(")") + s + ")" * s.count("(")
492+
493+
def gen_random(self):
494+
t = ""
495+
depth = 0
496+
while depth > 0 or self.random.randrange(10):
497+
t += self.random.choice([self.random.pseudo_word(min_len=0, max_len=3), "(", "("] + [")", ")", ")"] * (depth > 0))
498+
depth = t.count("(") - t.count(")")
499+
a, b = sorted([self.random.randrange(len(t) + 1) for _ in range(2)])
500+
s = t[a:b]
501+
if 5 < len(s) < 60:
502+
self.add(dict(s=s))
503+
480504

481505
if __name__ == "__main__":
482506
PuzzleGenerator.debug_problems()

generators/chess.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ def sat(squares: List[List[int]], m=8, n=8):
2626
return 4 * k == len({t for i, j in squares for t in [('row', i), ('col', j), ('SE', i + j), ('NE', i - j)]})
2727

2828
@staticmethod
29-
def sol(m, n): # brute force
29+
def sol(m, n):
30+
# brute force
3031
k = min(m, n)
3132

3233
from itertools import permutations
@@ -95,7 +96,8 @@ def sat(tour: List[List[int]], m=8, n=8):
9596
return sorted(tour) == [[i, j] for i in range(m) for j in range(n)] # cover every square once
9697

9798
@staticmethod
98-
def sol(m, n): # using Warnsdorff's heuristic, breaking ties randomly
99+
def sol(m, n):
100+
# using Warnsdorff's heuristic, breaking ties randomly
99101
import random
100102
for seed in range(100):
101103
r = random.Random(seed)

generators/classic_puzzles.py

Lines changed: 229 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
445672
class 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

Comments
 (0)