diff --git a/main.py b/main.py index 0730604..503a919 100644 --- a/main.py +++ b/main.py @@ -28,72 +28,6 @@ BOX_H_LABELS = 'ABCDEF' BOX_V_LABELS = '654321' -class Block(object): - def __init__(self, boxes, operation, result): - self.boxes = boxes - self.operation = operation - self.result = result - - self.solutions = set() - - self.verify() - - def verify(self): - assert self.operation in OP_LAMBDAS - if self.operation in [OP_NONE]: - assert len(self.boxes) == 1 - elif self.operation in [OP_MINUS, OP_DIVIDE]: - assert len(self.boxes) == 2 - else: - assert len(self.boxes) > 1 - for box in self.boxes: - assert len(box) == 2 - assert box[0] in BOX_H_LABELS - assert box[1] in BOX_V_LABELS - - def all_boxes_in_one_row_or_column(self): - is_same_column, is_same_row = True, True - for box in self.boxes[1:]: - if self.boxes[0][0] != box[0]: - is_same_column = False - if self.boxes[0][1] != box[1]: - is_same_row = False - return is_same_row or is_same_column - - def generate_combinations(self, k, n): - assert 1 <= k < n - # Exploit the structure of the problem - # For block of size 2, we don't need comb with replacement as they will - # neccesarily be in different columns/rows - if k == 2: - return list(combinations(range(1, n + 1), k)) - # for 3 and more we don't need if all of the boxes are on the - # same row or same column - elif k > 2: - if self.all_boxes_in_one_row_or_column(): - return list(combinations(range(1, n + 1), k)) - return list(combinations_with_replacement(range(1, n + 1), k)) - - def generate_hypotheses(self, grid_size): - rv = [] - op = OP_LAMBDAS[self.operation] - hypotheses = self.generate_combinations(k=len(self.boxes), n=grid_size) - for hypothesis in hypotheses: - if op(hypothesis) == self.result: - rv.append(hypothesis) - return rv - - def generate_solutions(self, grid_size): - self.generate_hypotheses(grid_size) - for hyp in self.generate_hypotheses(grid_size): - for perm in itertools.permutations(hyp): - sol = tuple(zip(self.boxes, perm)) - self.solutions.add(sol) - - def __repr__(self): - return 'Block {}'.format(self.boxes) - - def translate_box_to_rc(box): return BOX_V_LABELS.index(box[1]), BOX_H_LABELS.index(box[0]) @@ -118,6 +52,98 @@ def fill_grid(grid, solution): return grid +def check_grid(grid): + for row in grid: + if not has_line_integrity(row): + return False + for col_id in range(len(grid)): + col = [grid[r][col_id] for r in range(len(grid))] + if not has_line_integrity(col): + return False + return True + + +def init_grid(grid_size): + return [[0] * grid_size for _ in range(grid_size)] + + +class Block(object): + def __init__(self, boxes, operation, result): + self.boxes = boxes + self.operation = operation + self.result = result + + self.solutions = set() + + self.verify() + + def verify(self): + """ + Verifies that the box location are valid in the range A-F and 1-6 + Operations are in +-*/ or None. + Where -/ have exactly 2, None has exactly 1. + """ + assert self.operation in OP_LAMBDAS + if self.operation in [OP_NONE]: + assert len(self.boxes) == 1 + elif self.operation in [OP_MINUS, OP_DIVIDE]: + assert len(self.boxes) == 2 + else: + assert len(self.boxes) > 1 + for box in self.boxes: + assert len(box) == 2 + assert box[0] in BOX_H_LABELS + assert box[1] in BOX_V_LABELS + + def all_boxes_in_one_row_or_column(self): + """Returns true if all of the boxes are in the same row OR the same column""" + is_same_column, is_same_row = True, True + for box in self.boxes[1:]: + if self.boxes[0][0] != box[0]: + is_same_column = False + if self.boxes[0][1] != box[1]: + is_same_row = False + return is_same_row or is_same_column + + def generate_combinations(self, k, n): + """Generates k|n combinations for 2 boxes or if they are in the same row OR column. + Generates k|n combinations with replacements for other cases.""" + assert 1 <= k < n + # Exploit the structure of the problem + # For block of size 2, we don't need comb with replacement as they will + # neccesarily be in different columns/rows + if k == 2: + return list(combinations(range(1, n + 1), k)) + # for 3 and more we don't need if all of the boxes are on the + # same row or same column + elif k > 2: + if self.all_boxes_in_one_row_or_column(): + return list(combinations(range(1, n + 1), k)) + return list(combinations_with_replacement(range(1, n + 1), k)) + + def generate_hypotheses(self, grid_size): + """Generates hypothesis for this block based on the operation and result.""" + rv = [] + op = OP_LAMBDAS[self.operation] + hypotheses = self.generate_combinations(k=len(self.boxes), n=grid_size) + for hypothesis in hypotheses: + if op(hypothesis) == self.result: + rv.append(hypothesis) + return rv + + def generate_solutions(self, grid_size): + """Generates possible solutions for the block, including the limiting row/column requirement.""" + self.generate_hypotheses(grid_size) + for hyp in self.generate_hypotheses(grid_size): + for perm in itertools.permutations(hyp): + sol = tuple(zip(self.boxes, perm)) + if check_grid(fill_grid(init_grid(grid_size), sol)): + self.solutions.add(sol) + + def __repr__(self): + return 'Block {}'.format(self.boxes) + + class Game(object): def __init__(self, grid_size, blocks): self.blocks = blocks @@ -127,10 +153,8 @@ class Game(object): for block in self.blocks: block.generate_solutions(self.grid_size) - def init_grid(self): - return [[0] * self.grid_size for _ in range(self.grid_size)] - def verify(self): + """Check that each block is found exactly once in the grid.""" found = set() for block in self.blocks: for box in block.boxes: @@ -138,30 +162,17 @@ class Game(object): found.add(box) assert len(found) == self.grid_size ** 2 - def find_blocks_in_row(self, row_num): - rv = set() - for block in self.blocks: - for box in block.boxes: - if int(box[1]) == row_num: - rv.add(block) - return rv - - def check_grid(self, grid): - for row in grid: - if not has_line_integrity(row): - return False - for col_id in range(self.grid_size): - col = [grid[r][col_id] for r in range(self.grid_size)] - if not has_line_integrity(col): - return False - return True - def solve(self, grid, current_block_id=0): + """ + Recursive solution - Start by filling up the first block. + For each solution that passes the row/column requirement, check the recursive solution + with the next block. Break when it's the last block and the row/col requirement is filled. + """ block = self.blocks[current_block_id] for solution in block.solutions: prev_grid = deepcopy(grid) grid = fill_grid(grid, solution) - if not self.check_grid(grid): + if not check_grid(grid): grid = prev_grid else: if current_block_id == len(self.blocks) - 1: @@ -189,7 +200,7 @@ def main(): Block(boxes=['D1', 'E1'], operation=OP_MINUS, result=1), Block(boxes=['F1', 'F2', 'F3', 'E3'], operation=OP_PLUS, result=16), ]) - grid = game.solve(grid=game.init_grid()) + grid = game.solve(grid=init_grid(game.grid_size)) for line in grid: print(line)