Created
August 23, 2025 09:37
-
-
Save kaya3/189ee0b6311c7694a5a3d28ea181427d to your computer and use it in GitHub Desktop.
Python script to try to construct a single-cage killer kropki-less sudoku, or prove there isn't one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# See https://puzzling.stackexchange.com/q/132979/99846 | |
# Converts from (x, y) coordinates to an index in `range(81)`. | |
def xy_to_index(x, y): | |
return x + 9 * y | |
# Converts from an index in `range(81)` to (x, y) coordinates. | |
def index_to_xy(i): | |
return i % 9, i // 9 | |
# Yields the indices of the neighbours of the given index. | |
def neighbours(i): | |
x, y = index_to_xy(i) | |
if x > 0: yield xy_to_index(x - 1, y) | |
if y > 0: yield xy_to_index(x, y - 1) | |
if x < 8: yield xy_to_index(x + 1, y) | |
if y < 8: yield xy_to_index(x, y + 1) | |
# Maps each index to its set of neighbouring indices. | |
all_neighbours = [set(neighbours(i)) for i in range(81)] | |
# Searches for cages of size `n` which include all of `including` and none of | |
# `excluding`. The `boundary` is the set of non-excluded indices adjacent to | |
# some index in `including`. Each cage is yielded as a sorted tuple. | |
def all_cages_with(including, boundary, excluding, n): | |
'''''' | |
if len(including) == n: | |
yield tuple(sorted(including)) | |
else: | |
excluding = set(excluding) | |
boundary = boundary - excluding | |
for i in boundary: | |
excluding.add(i) | |
yield from all_cages_with( | |
including + (i,), | |
boundary | all_neighbours[i], | |
excluding, | |
n, | |
) | |
# Yields all cages of size `n`. This function doesn't find any duplicates, but | |
# just to be sure, we call `set(all_cages(n))` later anyway. | |
def all_cages(n): | |
excluding = set() | |
for i in range(81): | |
excluding.add(i) | |
yield from all_cages_with((i,), all_neighbours[i], excluding, n) | |
# Returns a dictionary counting how many grids have each possible sum in the | |
# given cage. Grids with any repeated digits in the cage are not counted. | |
def cage_histogram(cage, grids): | |
n = len(cage) | |
histogram = dict() | |
for grid in grids: | |
if len({grid[i] for i in cage}) == n: | |
s = sum(grid[i] for i in cage) | |
histogram[s] = histogram.get(s, 0) + 1 | |
return histogram | |
# Searches for and prints (cage, sum) pairs for which a single-cage killer | |
# sudoku would have few solutions. | |
def main(all_solutions): | |
min_solutions = 4 | |
count = 0 | |
for n in range(2, 10): | |
cages = set(all_cages(n)) | |
print('Found', len(cages), 'cages of size', n) | |
for cage in cages: | |
histogram = cage_histogram(cage, all_solutions) | |
for cage_sum, num_solutions in histogram.items(): | |
if num_solutions <= min_solutions: | |
count = (0 if num_solutions < min_solutions else count) + 1 | |
min_solutions = num_solutions | |
print('Only', num_solutions, 'solutions for sum =', cage_sum, 'in cage', cage) | |
print('Found', count, 'cages with', min_solutions, 'solutions') | |
if __name__ == '__main__': | |
all_solutions = [ | |
tuple(map(int, grid.strip())) | |
for grid in open('all_solutions.txt').readlines() | |
] | |
main(all_solutions) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment