def solve_puzzle_mosaic(counts, *, height, width): board = IntMatrix('b', nb_rows=height, nb_cols=width) at_ = lambda l, c: board[l][c] # There is no empty cell: complete_c = [Xor(cell == WHITE, cell == BLACK) for cell in flatten(board)] def valid_cells_in_disk(l, c): geom = {'height': height, 'width': width} cells = cells_in_disk((l, c), radius=1) return [cell for cell in cells if inside_board(cell, **geom)] def gen_sing_count_const(l, c): cell_vars = [at_(*cell) for cell in valid_cells_in_disk(l, c)] # If we don't use sum we may express the constraint with `Exactly()` return Sum(cell_vars) == counts[l][c] count_c = [ gen_sing_count_const(l, c) for l, c in itertools.product(range(height), range(width)) if counts[l][c] > 0 ] s = Solver() s.add(complete_c + count_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in board]
def solve_killer_sudoku(puzzle, *, sums): X = IntMatrix('n', ORDER, ORDER) at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] latin_c = gen_latin_square_constraints(X, ORDER) nonet_c = [] nonets_inds = get_same_block_indices(NONETS_IDS) for board_indices in nonets_inds.values(): vars_ = get_vars_at(board_indices) nonet_c.append(Distinct(vars_)) #sum of distinct numbers cage_c = [] cages_inds = get_same_block_indices(puzzle) for sums_ind, cage_indices in cages_inds.items(): vars_ = get_vars_at(cage_indices) cstrnt = And(Distinct(vars_), Sum(vars_) == sums[sums_ind]) cage_c.append(cstrnt) s = Solver() s.add(latin_c + nonet_c + cage_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in X]
def solve_hidato(puzzle, *, order, maximum): X = IntMatrix('n', order, order) vars_ = list(itertools.chain(*X)) vals = list(itertools.chain(*puzzle)) # occupied cells == cells belonging to the solution # filled cells == cells already filled # a filled cell is a occupied cell. # An occupied cell would be a filled cell at the end of the solution # occup == -1 for holes, and unoccupied cells occupied_cells = [cell for cell, occup in zip(vars_, vals) if occup > -1] range_c = [And(cell >= 1, cell <= maximum) for cell in occupied_cells] distinct_c = [Distinct(occupied_cells)] # cell == 0 for cells not yet filled # So cell > 0 contains cells already filled instance_c = [cell == val for cell, val in zip(vars_, vals) if val > 0] at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] def gen_consecutive_nums_constraint(l, c): val_neighs = [ neigh for neigh in neighbours(l, c) if inside_board(neigh, height=order, width=order) ] neighs = get_vars_at(val_neighs) occupied_neighs = set(neighs).intersection(occupied_cells) var = at_(l, c) #content of current cell one_adj_cell_is_consec_c = Exactly( *[adj == var + 1 for adj in occupied_neighs], 1) current_cell_is_max_c = var == maximum return Or(current_cell_is_max_c, one_adj_cell_is_consec_c) consecutive_c = [] for l, row_vals in enumerate(puzzle): for c, val in enumerate(row_vals): if val >= 0: constrnt = gen_consecutive_nums_constraint(l, c) consecutive_c.append(constrnt) s = Solver() s.add(range_c + instance_c + distinct_c + consecutive_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in X]
def solve_kakuro(horiz_cages, vertic_cages, horiz_clues, vertic_clues, *, order): X = IntMatrix('n', order, order) at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] # Look for any discrepancies between horiz and vertic occupancies. for h_row, v_row in itertools.zip_longest(horiz_cages, vertic_cages): for l, c in itertools.zip_longest(h_row, v_row): if l < 0 or c < 0: assert (l, c) == (-1, -1) vars_ = itertools.chain(*X) # We have already verified that horiz_cages and vertic_cages have same occupancies occupancy = itertools.chain(*horiz_cages) # digit range_c = [ And(cell >= 1, cell <= 9) for cell, occup in zip(vars_, occupancy) if occup > -1 ] h_cage_c = [] h_cages_inds = get_same_block_indices(horiz_cages) for h_clue_ind, h_cage_indices in h_cages_inds.items(): if h_clue_ind > -1: # cell is occupied vars_ = get_vars_at(h_cage_indices) cnstrnt = And(Distinct(vars_), Sum(vars_) == horiz_clues[h_clue_ind]) h_cage_c.append(cnstrnt) v_cage_c = [] v_cages_inds = get_same_block_indices(vertic_cages) for v_clue_ind, v_cage_indices in v_cages_inds.items(): if v_clue_ind > -1: # cell is occupied vars_ = get_vars_at(v_cage_indices) cnstrnt = And(Distinct(vars_), Sum(vars_) == vertic_clues[v_clue_ind]) v_cage_c.append(cnstrnt) s = Solver() s.add(range_c + h_cage_c + v_cage_c) s.check() m = s.model() res = [[m[s] for s in row] for row in X] return res
def solve_pattern(runs_columnwise, runs_rowwise, height, width): X = IntMatrix('c', nb_rows=height, nb_cols=width) assert len(runs_rowwise) == len(X) == height rowwise_c = [ gen_constraints_vars_runs(row, runs) for row, runs in zip(X, runs_rowwise)] X_trans = transpose(X) assert len(runs_columnwise) == len(X_trans) == width colwise_c = [ gen_constraints_vars_runs(row, runs) for row, runs in zip(X_trans, runs_columnwise)] s = Solver() s.add( rowwise_c + colwise_c ) s.check() m = s.model() return [ [ m[cell] for cell in row] for row in X ]
def solve_puzzle_takuzu(puzzle, *, height, width): board = IntMatrix('b', nb_rows=height, nb_cols=width) assert width % 2 == 0, 'Width must be pair' assert height % 2 == 0, 'Height must be pair' vars_ = flatten(board) vals = flatten(puzzle) instance_c = [var == val for var, val in zip(vars_, vals) if val > -1] complete_c = [Xor(cell == 0, cell == 1) for cell in flatten(board)] # Equal number of 1s and 0s: so there are n/2 1s and n/2 0s _horiz_count_c = [Sum(row) == width / 2 for row in board] _vertical_count_c = [Sum(row) == height / 2 for row in transpose(board)] count_c = _horiz_count_c + _vertical_count_c # Rows or columns are unique def get_id(sequence): base = 2 return Sum([val * base**pos for pos, val in enumerate(sequence)]) row_unique_c = [Distinct([get_id(row) for row in board])] col_unique_c = [Distinct([get_id(row) for row in transpose(board)])] # No more than 2 adjacent 1s or 0s def gen_adj_constraints(sequence): adjs = windowed(sequence, 3) # if three adjacent boxes are 0(resp 1) , sum is 0(resp 3). # Among the eight possibilities of three adjacent boxes return [And(Sum(adj) != 0, Sum(adj) != 3) for adj in adjs] _row_adj_c = flatten([gen_adj_constraints(row) for row in board]) _col_adj_c = flatten( [gen_adj_constraints(row) for row in transpose(board)]) adj_c = _row_adj_c + _col_adj_c s = Solver() s.add(instance_c + complete_c + count_c + row_unique_c + col_unique_c + adj_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in board]
def solve_puzzle_shikaku(puzzle, *, height, width): X = IntMatrix('r', nb_rows=height, nb_cols=width) assert sum( flatten(puzzle) ) == height * width, "Sum of the areas of all rectangles must be equal to board area" at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] def get_unique_rect_id(l, c): return l * width + c + 1 rectangle_area_c = [] for l, c in itertools.product(range(height), range(width)): rect_area = puzzle[l][c] if rect_area > 0: rect_id = get_unique_rect_id(l, c) rects = get_possible_rectangles_at((l, c), height=height, width=width, rect_area=rect_area) rectangle_configs_with_this_id = [] for rect in rects: indices = list(get_indices_in_rect(rect)) rect_vars = get_vars_at(indices) # This is like "coloring" the rectangle with id cnstrnt = coerce_eq(rect_vars, [rect_id] * len(rect_vars)) rectangle_configs_with_this_id.append(cnstrnt) rectangle_area_c.append(Exactly(*rectangle_configs_with_this_id, 1)) s = Solver() s.add(rectangle_area_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in X]
def solve_unequal(puzzle, *, order, lt_inequalities): X = IntMatrix('n', order, order) latin_c = gen_latin_square_constraints(X, order) vars_ = flatten(X) vals = flatten(puzzle) instance_c = [var == val for var, val in zip(vars_, vals) if val > 0] at_ = lambda l, c: X[l][c] lesser_than_c = [at_(*lhs) < at_(*rhs) for lhs, rhs in lt_inequalities] s = Solver() s.add(latin_c + instance_c + lesser_than_c) s.check() m = s.model() res = [[m[s] for s in row] for row in X] return res
def solve_tower_puzzle(n, top, left, right, bottom, instance=None): knowl = gen_knowl_dict(n) X = IntMatrix('h', n, n) X_trans = transpose(X) assert len(top) == len(left) == n assert len(right) == len(bottom) == n latin_c = gen_latin_square_constraints(X, n) left_c = [ constrain_towers(row, h, knowl) for row, h in zip(X, left) if h > 0 ] right_c = [ constrain_towers(row[::-1], h, knowl) for row, h in zip(X, right) if h > 0 ] top_c = [ constrain_towers(row, h, knowl) for row, h in zip(X_trans, top) if h > 0 ] bottom_c = [ constrain_towers(row[::-1], h, knowl) for row, h in zip(X_trans, bottom) if h > 0 ] s = Solver() s.add(latin_c + left_c + right_c + top_c + bottom_c) if instance is not None: for row_v, row in zip(X, instance): for var, value in zip(row_v, row): if value > 0: s.add(var == value) s.check() m = s.model() res = [[m[s] for s in row] for row in X] return res
def solve_puzzle_range(puzzle, *, height, width): board = IntMatrix('b', nb_rows=height, nb_cols=width) pars = {'board': board, 'width': width, 'height': height} contig_c = gen_contiguous_constraints(puzzle, **pars) # There is no empty cell: complete_c = [Xor(cell == WHITE, cell == BLACK) for cell in flatten(board)] # No two black squares are adjacent: horizontal horiz_bl_c = [[ And(cell_1 == BLACK, cell_2 == BLACK) for cell_1, cell_2 in zip(row, row[1:]) ] for row in board] horiz_bl_c = flatten(horiz_bl_c) horiz_bl_c = AtMost(*horiz_bl_c, 0) # No two black squares are adjacent: vertical vertic_bl_c = [[ And(cell_1 == BLACK, cell_2 == BLACK) for cell_1, cell_2 in zip(row, row[1:]) ] for row in transpose(board)] vertic_bl_c = flatten(vertic_bl_c) vertic_bl_c = AtMost(*vertic_bl_c, 0) # No two black squares are adjancent: vertically or horizontall (ortho) ortho_bl_c = [horiz_bl_c, vertic_bl_c] s = Solver() s.add(contig_c + complete_c + ortho_bl_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in board]
def solve_keen(puzzle, *, order, arithmetic_constraints): X = IntMatrix('n', order, order) at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] arith_c = [] blocks_inds = get_same_block_indices(puzzle) for constr_ind, board_indices in blocks_inds.items(): op, result = arithmetic_constraints[constr_ind] vars_ = get_vars_at(board_indices) if op == 'm': arith_c.append(Product(vars_) == result) elif op == 'a': arith_c.append(Sum(vars_) == result) elif op == 's': assert len(vars_) == 2, 'Subtraction needs exactly two operands' a, b = vars_ arith_c.append(Or(a - b == result, b - a == result)) elif op == 'd': assert len(vars_) == 2, 'Division needs exactly two operands' a, b = vars_ arith_c.append(Or(a / b == result, b / a == result)) latin_c = gen_latin_square_constraints(X, order) s = Solver() s.add(latin_c + arith_c) s.check() m = s.model() res = [[m[s] for s in row] for row in X] return res
def solve_puzzle_range(puzzle, *, height, width): board = IntMatrix('b', nb_rows=height, nb_cols=width) pars = {'board': board, 'width': width, 'height': height} vars_ = flatten(board) vals = flatten(puzzle) instance_c = [var == val for var, val in zip(vars_, vals) if val > 0] contig_c = gen_contiguous_constraints(puzzle, **pars) # There is no empty cell: And cell values range from 1 to height*width complete_c = [ And(0 < cell, cell < height * width + 1) for cell in flatten(board) ] # The model wants the numbers outside the block to be different than # the number (count) in the block. But to resolve this constraint it # uses big numbers 32, 211 ... until the max we've put. # It doesn't try 1 there. # So it exhausts (if we have two 1) 221 * 221 = 48841 possbilities. # We want to avoid that by saying Except for 1 all other counts have at least # one neighbour that is equal to it. # We have dodged the problem by using an optimizer (minimize cost). # But it is better to encode and incoroporate this constraint in the model. geom = { 'width': width, 'height': height } at_ = lambda l, c : board[l][c] def valid_neighbours(l, c): return [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, **geom) ] def at_least_one_neighbour_is_same(l, c): return Or([ at_(l, c) == at_(*neigh) for neigh in valid_neighbours(l, c) ]) ortho_neigh_c = [ Xor( board[l][c] == 1, at_least_one_neighbour_is_same(l, c)) for l, c in itertools.product(range(height), range(width)) ] # s = Solver() # s.add(complete_c + contig_c + instance_c + ortho_neigh_c) # s.check() # m = s.model() # solution_model = [ [ m[cell].as_long() for cell in row] for row in board ] # return solution_model opt = Optimize() opt.add(complete_c + contig_c + instance_c + ortho_neigh_c) cost = Int('cost') opt.add(cost == Sum(vars_)) h = opt.minimize(cost) opt.check() opt.lower(h) m = opt.model() solution_model = [ [ m[cell].as_long() for cell in row] for row in board ] optimizer_check_model = opt while True: print('checking solution') print(solution_model) optimizer_check_model.push() contig_model_c = gen_contiguous_constraints(solution_model, **pars) instance_model_c = [ var == val for var, val in zip(vars_, flatten(solution_model)) if val > 0] # check that the numbers in the solution verifies the # contiguouness constraint optimizer_check_model.add(contig_model_c) optimizer_check_model.add(instance_model_c) # redundant? if optimizer_check_model.check() == sat: return solution_model #HERE IS THE RETURN STATEMENT print('rejecting solution') print(solution_model) optimizer_check_model.pop() # the rejected solution is added after 'pop'. This is how it is # added to the model optimizer_check_model.add(Not(And(instance_model_c))) optimizer_check_model.check() optimizer_check_model.lower(h) m = optimizer_check_model.model() solution_model = [ [ m[cell].as_long() for cell in row ] for row in board ]
def solve_magnets(*, tiles, positive_horizontal, positive_vertical, negative_vertical, negative_horizontal, height, width): X = IntMatrix('m', nb_rows=height, nb_cols=width) # Let us concentrate on edge|pole|half of the tile|domino|magnet halves = flatten(X) # completeness complete_c = [ Or([edge == POSITIVE, edge == NEGATIVE, edge == NEUTRAL]) for edge in halves ] assert len(positive_vertical) == len(X) pos_vertic_c = [ coerce_charge(row, POSITIVE, pos_v) for row, pos_v in zip(X, positive_vertical) if pos_v >= 0 ] assert len(negative_vertical) == len(X) neg_vertic_c = [ coerce_charge(row, NEGATIVE, neg_v) for row, neg_v in zip(X, negative_vertical) if neg_v >= 0 ] X_trans = transpose(X) assert len(positive_horizontal) == len(X_trans) pos_horiz_c = [ coerce_charge(row, POSITIVE, pos_h) for row, pos_h in zip(X_trans, positive_horizontal) if pos_h >= 0 ] assert len(negative_horizontal) == len(X_trans) neg_horiz_c = [ coerce_charge(row, NEGATIVE, neg_h) for row, neg_h in zip(X_trans, negative_horizontal) if neg_h >= 0 ] def horiz_tile(l, c): return X[l][c], X[l][c + 1] def vertic_tile(l, c): return X[l][c], X[l + 1][c] horiz_tiles_c = [ coerce_tile(*horiz_tile(l, c)) for l, c in itertools.product(range(height), range(width)) if tiles[l][c] == '>' ] vertic_tiles_c = [ coerce_tile(*vertic_tile(l, c)) for l, c in itertools.product(range(height), range(width)) if tiles[l][c] == 'v' ] # edge1 edge2 may not belong to the same magnet|tile horiz_neigh_c = [[ coerce_neigh(edge1, edge2) for edge1, edge2 in zip(row, row[1:]) ] for row in X] horiz_neigh_c = flatten(horiz_neigh_c) vertic_neigh_c = [[ coerce_neigh(edge1, edge2) for edge1, edge2 in zip(row, row[1:]) ] for row in X_trans] vertic_neigh_c = flatten(vertic_neigh_c) s = Solver() s.add(complete_c + pos_vertic_c + pos_horiz_c + neg_vertic_c + neg_horiz_c + horiz_tiles_c + vertic_tiles_c + horiz_neigh_c + vertic_neigh_c) s.check() m = s.model() return [[m[edge] for edge in row] for row in X]
def solve_puzzle_dominosa(puzzle, *, height, width, order): board = IntMatrix('d', nb_rows=height, nb_cols=width) complete_c = [ Exactly( cell == VERTIC_START, cell == VERTIC_END, cell == HORIZ_START, cell == HORIZ_END, 1) for cell in flatten(board) ] _no_aberrant_horiz_c = [] for row in board: for domino in pairwise(row): # not-head and tail abberrant_1 = And(domino[0] != HORIZ_START, domino[1] == HORIZ_END) # head and not-tail _no_aberrant_horiz_c.append(Not(abberrant_1)) abberrant_2 = And(domino[0] == HORIZ_START, domino[1] != HORIZ_END) _no_aberrant_horiz_c.append(Not(abberrant_2)) _no_aberrant_vertic_c = [] for row in transpose(board): for domino in pairwise(row): # not-head and tail abberrant_1 = And(domino[0] != VERTIC_START, domino[1] == VERTIC_END) _no_aberrant_vertic_c.append(Not(abberrant_1)) # head and not-tail abberrant_2 = And(domino[0] == VERTIC_START, domino[1] != VERTIC_END) _no_aberrant_vertic_c.append(Not(abberrant_2)) _no_aberrant_horiz_border_cell_c = [And(row[0] != HORIZ_END, row[-1] != HORIZ_START) for row in board] _no_aberrant_vertic_border_cell_c = [And(row[0] != VERTIC_END, row[-1] != VERTIC_START) for row in transpose(board)] no_aberrant_c = ( _no_aberrant_horiz_c + _no_aberrant_horiz_border_cell_c + _no_aberrant_vertic_c + _no_aberrant_vertic_border_cell_c ) # By taking both the start_variable and end_variable we may not # need no_aberrant_c ... but it's better separated this way. unique_c = [] locs_h = defaultdict(list) #locations for var_row, row in zip(board, puzzle): for var, dom in zip(var_row, pairwise(row)): locs_h[normalize_domino(dom)].append(var) locs_v = defaultdict(list) for var_row, row in zip(transpose(board), transpose(puzzle)): for var, dom in zip(var_row, pairwise(row)): locs_v[normalize_domino(dom)].append(var) # normalized domino for n_domino in itertools.combinations_with_replacement(range(order + 1), 2): if n_domino in locs_h and n_domino in locs_v: only_one_such_domino = Exactly(*[edge == VERTIC_START for edge in locs_v[n_domino]], *[edge == HORIZ_START for edge in locs_h[n_domino]], 1) unique_c.append(only_one_such_domino) elif n_domino not in locs_h: only_one_such_domino = Exactly(*[edge == VERTIC_START for edge in locs_v[n_domino]], 1) unique_c.append(only_one_such_domino) elif n_domino not in locs_v: only_one_such_domino = Exactly(*[edge == HORIZ_START for edge in locs_h[n_domino]], 1) unique_c.append(only_one_such_domino) s = Solver() s.add(complete_c + no_aberrant_c + unique_c) s.check() m = s.model() return [ [m[cell] for cell in row] for row in board]
def solve_tents(board, *, height, width, horizontal_tents, vertical_tents): preprocessed = preprocess(board, height=height, width=width) X = IntMatrix('t', nb_rows=height, nb_cols=width) at_ = lambda l, c: X[l][c] is_tree = lambda l, c: preprocessed[l][c] > 0 # the coupling between a tree and a tent is unique. Think tile|domino def gen_coupling_constraints(l, c): val_neighs = [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, height=height, width=width) ] couplings = [X[l][c] == -at_(*cell) for cell in val_neighs] # we don't add constraint that it is unoccupied by a tree # it's taken care by another set of constraints # return Exactly(*couplings, 1) return Exactly(*couplings, 1) # No two tents can be adjacent to each other. # None of the 8 directions. (think: King's move in chess). # The description above is the tent's point of view # If we take the board's point of view: # A square of 4 cells can contain at most 1 tree def gen_proximity_constraints(l, c): square = [ (l, c), (l, c + 1), # towards right (l + 1, c), (l + 1, c + 1) ] # towards bottom and right val_cells_in_square = [ cell for cell in square if inside_board(cell, height=height, width=width) ] # cell < 0 : we have encoded as tent tents_in_a_square = [at_(*cell) < 0 for cell in val_cells_in_square] return AtMost(*tents_in_a_square, 1) def coerce_nb_tents(cells, count_): tents = [cell < 0 for cell in cells] return Exactly(*tents, count_) MAX = height * width complete_c = [And(-MAX < cell, cell < MAX) for cell in flatten(X)] coupling_c = [ gen_coupling_constraints(l, c) for l, c in itertools.product(range(height), range(width)) if is_tree(l, c) ] instance_c = [ at_(l, c) == preprocessed[l][c] for l, c in itertools.product(range(height), range(width)) if preprocessed[l][c] > 0 ] # In generating constraints we wander towards right and towards bottom # So : height - 1, width - 1 tree_proximity_c = [ gen_proximity_constraints(l, c) for l, c in itertools.product(range(height - 1), range(width - 1)) ] NB_TREES = sum(cell > 0 for cell in flatten(preprocessed)) same_number_trees_tents_c = [coerce_nb_tents(flatten(X), NB_TREES)] # vertical tents means nb tents in each line. Noted vertically on the board # on the left or right side assert len(vertical_tents) == height == len(X) row_sums_c = [ coerce_nb_tents(row, count_v) for row, count_v in zip(X, vertical_tents) ] X_trans = transpose(X) # horizontal tents means nb tents in each line. Noted horizontally on the board # at the bottom of the board or top of the board assert len(horizontal_tents) == width == len(X_trans) # row in X_trans means col in X col_sums_c = [ coerce_nb_tents(row, count_h) for row, count_h in zip(X_trans, horizontal_tents) ] # NOTE: if ever col_sums or row_sums are partially erased to add difficulty. # use -1. and add here if count_h >= 0 s = Solver() s.add(complete_c + instance_c + coupling_c + tree_proximity_c + same_number_trees_tents_c + row_sums_c + col_sums_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in X]
def solve_akari(puzzle, *, height, width): X = IntMatrix('n', nb_rows=height, nb_cols=width) cell_vars = flatten(X) clues = flatten(puzzle) assert len(cell_vars) == len( clues), "Discrepancy between puzzle and cell-variables" complete_c = [] for cell_var, clue in zip(cell_vars, clues): if clue == WALL or clue in (0, 1, 2, 3, 4): complete_c.append(cell_var == WALL) else: complete_c.append( Xor(cell_var == LIGHT_BULB, cell_var == NOT_LIGHT_BULB)) geom = {'width': width, 'height': height} at_ = lambda l, c: X[l][c] def valid_neighbours(l, c): return [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, **geom) ] def gen_count_lightbulb_constraint(l, c, count_lightbulbs): neigh_as_light_bulb = [ at_(*cell) == LIGHT_BULB for cell in valid_neighbours(l, c) ] return Exactly(*neigh_as_light_bulb, count_lightbulbs) count_surrounding_light_bulbs_c = [] for l, c in itertools.product(range(height), range(width)): clue = puzzle[l][c] if clue in (0, 1, 2, 3, 4): cnstr = gen_count_lightbulb_constraint(l, c, count_lightbulbs=clue) count_surrounding_light_bulbs_c.append(cnstr) def get_vars_at(indices): return [at_(*ind) for ind in indices] # think of this as vectorialization of 'value' def vars_eq_scalar(variabs, value): return [var == value for var in variabs] horizontal_cages_indexed = get_horizontal_cages(puzzle) vertical_cages_indexed = get_vertical_cages(puzzle) horizontal_cages = set( tuple(sorted(cage)) for cage in horizontal_cages_indexed.values()) vertical_cages = set( tuple(sorted(cage)) for cage in vertical_cages_indexed.values()) # two lightbulbs can't illuminate each other. territory? forbid encroachment? # there can be at most one bulb in each horizontal cage _h_no_encroachment_c = [] for hcage in horizontal_cages: light_bulbs_in_cage = vars_eq_scalar(get_vars_at(hcage), LIGHT_BULB) cnstrnt = AtMost(*light_bulbs_in_cage, 1) _h_no_encroachment_c.append(cnstrnt) _v_no_encroachment_c = [] for vcage in vertical_cages: light_bulbs_in_cage = vars_eq_scalar(get_vars_at(vcage), LIGHT_BULB) cnstrnt = AtMost(*light_bulbs_in_cage, 1) _v_no_encroachment_c.append(cnstrnt) no_encroachment_c = _h_no_encroachment_c + _v_no_encroachment_c assert sorted(tuple(vertical_cages_indexed.keys())) == sorted( tuple(horizontal_cages_indexed.keys()) ), "verical cages and horizontal cages must have same keys" all_empty_cells_illuminated_c = [] for coord in vertical_cages_indexed.keys(): vcage = vertical_cages_indexed[coord] hcage = horizontal_cages_indexed[coord] horiz_cage_has_lightbulb = Exactly( *vars_eq_scalar(get_vars_at(hcage), LIGHT_BULB), 1) vertic_cage_has_lightbulb = Exactly( *vars_eq_scalar(get_vars_at(vcage), LIGHT_BULB), 1) cnstrnt = Or(horiz_cage_has_lightbulb, vertic_cage_has_lightbulb) all_empty_cells_illuminated_c.append(cnstrnt) s = Solver() s.add(complete_c + count_surrounding_light_bulbs_c + no_encroachment_c + all_empty_cells_illuminated_c) s.check() m = s.model() return [[m[cell] for cell in row] for row in X]
def solve_tracks(*, tracks, start_index, end_index, horizontal_clues, vertical_clues, height, width): X = IntMatrix('t', nb_rows=height, nb_cols=width) assert sum(horizontal_clues) == sum(vertical_clues) nb_occupied_cells = sum(horizontal_clues) MAX = nb_occupied_cells cells = list(itertools.chain(*X)) range_c = [And(n >= 0, n <= nb_occupied_cells) for n in cells] at_ = lambda l, c: X[l][c] extremities_c = [at_(*start_index) == 1, at_(*end_index) == MAX] for index_, pattern in tracks: l, c = index_ bottom, left, top, right = map(int, list(pattern)) bottom, left, top, right = bottom == 1, left == 1, top == 1, right == 1 if index_ == start_index: # the starting track is the leftmost # so it has no left if bottom: nxt = l + 1, c elif top: nxt = l - 1, c elif right: nxt = l, c + 1 curr_var = at_(*index_) nxt_var = at_(*nxt) extremities_c.append(nxt_var == curr_var + 1) elif index_ == end_index: # the ending track is the bottommost # so it has no bottom if top: prv = l - 1, c if right: prv = l, c + 1 if left: prv = l, c - 1 curr_var = at_(*index_) prv_var = at_(*prv) extremities_c.append(curr_var == prv_var + 1) X_trans = transpose(X) # Exactly count_ occupied (> 0) cells in each row row_sums_c = [ Exactly(*[cell > 0 for cell in row], count_) for row, count_ in zip(X, vertical_clues) ] row_sums_c = [ Exactly(*[cell > 0 for cell in row], count_) for row, count_ in zip(X, vertical_clues) ] col_sums_c = [ Exactly(*[cell > 0 for cell in row], count_) for row, count_ in zip(X_trans, horizontal_clues) ] # we give unique_id to unoccupied cell. for using Distinct on occupied cells distinct_c = Distinct( [If(cell > 0, cell, -i) for i, cell in enumerate(cells, 1)]) # Every occupied cell is at a given 'distance' to the start. # No other cell has the same distance distinct_c = [distinct_c] def get_adj_track_indices(index_, pattern): l, c = index_ bottom, left, top, right = map(int, list(pattern)) bottom, left, top, right = bottom == 1, left == 1, top == 1, right == 1 res = [] if left: # the starting track is the leftmost if index_ != start_index: res.append((l, c - 1)) if right: res.append((l, c + 1)) if top: res.append((l - 1, c)) if bottom: # the ending track is the bottommost if index_ != end_index: res.append((l + 1, c)) res.insert(1, index_) return res at_ = lambda l, c: X[l][c] def get_vars_at(indices): return [at_(*ind) for ind in indices] def coerce_sequential(vars_): curr, *succs = vars_ ascending_c = And([nxt == curr + i for i, nxt in enumerate(succs, 1)]) descending_c = And([nxt == curr - i for i, nxt in enumerate(succs, 1)]) return Or(ascending_c, descending_c) # constraint forcing parts of the tracks to be in ascending or descending order sequential_c = [] for index_, pattern in tracks: inds = get_adj_track_indices(index_, pattern) successive_track_vars = get_vars_at(inds) cnstrnt = coerce_sequential(successive_track_vars) sequential_c.append(cnstrnt) # The start and end track produces an absurd condition. but it would be weeded out by other constraints. def gen_consecutive_nums_constraint(l, c): geom = {'height': height, 'width': width} val_neighs = [ neigh for neigh in neighbours((l, c)) if inside_board(neigh, **geom) ] neighs = get_vars_at(val_neighs) var = at_(l, c) #content of current cell one_adj_cell_is_consec_c = Exactly(*[adj == var + 1 for adj in neighs], 1) current_cell_is_max_c = var == MAX unoccupied_c = var == 0 return Or(unoccupied_c, current_cell_is_max_c, one_adj_cell_is_consec_c) # constraint forcing one of the adjacent cells to be the 'successor' successor_c = [ gen_consecutive_nums_constraint(l, c) for l, c in itertools.product(range(height), range(width)) ] s = Solver() s.add(range_c + extremities_c + row_sums_c + col_sums_c + distinct_c + sequential_c + successor_c) s.check() m = s.model() res = [[m[s] for s in row] for row in X] return res