from ortools.constraint_solver import pywrapcp from collections import deque small = [[3, 2, -1, 3], [-1, -1, -1, 2], [3, -1, -1, -1], [3, -1, 3, 1]] medium = [[-1, 0, -1, 1, -1, -1, 1, -1], [-1, 3, -1, -1, 2, 3, -1, 2], [-1, -1, 0, -1, -1, -1, -1, 0], [-1, 3, -1, -1, 0, -1, -1, -1], [-1, -1, -1, 3, -1, -1, 0, -1], [1, -1, -1, -1, -1, 3, -1, -1], [3, -1, 1, 3, -1, -1, 3, -1], [-1, 0, -1, -1, 3, -1, 3, -1]] big = [[3, -1, -1, -1, 2, -1, 1, -1, 1, 2], [1, -1, 0, -1, 3, -1, 2, 0, -1, -1], [-1, 3, -1, -1, -1, -1, -1, -1, 3, -1], [2, 0, -1, 3, -1, 2, 3, -1, -1, -1], [-1, -1, -1, 1, 1, 1, -1, -1, 3, 3], [2, 3, -1, -1, 2, 2, 3, -1, -1, -1], [-1, -1, -1, 1, 2, -1, 2, -1, 3, 3], [-1, 2, -1, -1, -1, -1, -1, -1, 2, -1], [-1, -1, 1, 1, -1, 2, -1, 1, -1, 3], [3, 3, -1, 1, -1, 2, -1, -1, -1, 2]] def NeighboringArcs(i, j, h_arcs, v_arcs): tmp = [] if j > 0: tmp.append(h_arcs[i][j - 1]) if j < len(v_arcs) - 1: tmp.append(h_arcs[i][j]) if i > 0: tmp.append(v_arcs[j][i - 1]) if i < len(h_arcs) - 1: tmp.append(v_arcs[j][i]) return tmp def PrintSolution(data, h_arcs, v_arcs): num_rows = len(data) num_columns = len(data[0]) for i in range(num_rows): first_line = '' second_line = '' third_line = '' for j in range(num_columns): h_arc = h_arcs[i][j].Value() v_arc = v_arcs[j][i].Value() cnt = data[i][j] first_line += ' ---' if h_arc else ' ' second_line += '|' if v_arc else ' ' second_line += ' ' if cnt == -1 else ' %i ' % cnt third_line += '| ' if v_arc == 1 else ' ' termination = v_arcs[num_columns][i].Value() second_line += '|' if termination else ' ' third_line += '|' if termination else ' ' print(first_line) print(third_line) print(second_line) print(third_line) last_line = '' for j in range(num_columns): h_arc = h_arcs[num_rows][j].Value() last_line += ' ---' if h_arc else ' ' print(last_line) class BooleanSumEven(pywrapcp.PyConstraint): def __init__(self, solver, vars): pywrapcp.PyConstraint.__init__(self, solver) self.__vars = vars self.__num_possible_true_vars = pywrapcp.NumericalRevInteger(0) self.__num_always_true_vars = pywrapcp.NumericalRevInteger(0) def Post(self): for i in range(len(self.__vars)): v = self.__vars[i] if not v.Bound(): demon = self.Demon(BooleanSumEven.Update, i) v.WhenBound(demon) def InitialPropagate(self): num_always_true = 0 num_possible_true = 0 possible_true_index = -1 for i in range(len(self.__vars)): var = self.__vars[i] if var.Min() == 1: num_always_true += 1 num_possible_true += 1 elif var.Max() == 1: num_possible_true += 1 possible_true_index = i if num_always_true == num_possible_true and num_possible_true % 2 == 1: self.solver().Fail() if num_possible_true == num_always_true + 1: self.__vars[possible_true_index].SetValue(num_always_true % 2) self.__num_possible_true_vars.SetValue(self.solver(), num_possible_true) self.__num_always_true_vars.SetValue(self.solver(), num_always_true) def Update(self, index): solver = self.solver() value = self.__vars[index].Value() if value == 0: self.__num_possible_true_vars.Decr(solver) else: self.__num_always_true_vars.Incr(solver) num_possible = self.__num_possible_true_vars.Value() num_always = self.__num_always_true_vars.Value() if num_always == num_possible and num_possible % 2 == 1: solver.Fail() if num_possible == num_always + 1: possible_true_index = -1 for i in range(len(self.__vars)): if not self.__vars[i].Bound(): possible_true_index = i break if possible_true_index != -1: self.__vars[possible_true_index].SetValue(num_always % 2) def DebugString(self): return 'BooleanSumEven' # Dedicated constraint: There is a single path on the grid. # This constraint does not enforce the non-crossing, this is done # by the constraint on the degree of each node. class GridSinglePath(pywrapcp.PyConstraint): def __init__(self, solver, h_arcs, v_arcs): pywrapcp.PyConstraint.__init__(self, solver) self.__h_arcs = h_arcs self.__v_arcs = v_arcs def Post(self): demon = self.DelayedInitialPropagateDemon() for row in self.__h_arcs: for var in row: var.WhenBound(demon) for column in self.__v_arcs: for var in column: var.WhenBound(demon) # This constraint implements a single propagation. # If one point is on the path, it checks the reachability of all possible # nodes, and zero out the unreachable parts. def InitialPropagate(self): num_rows = len(self.__h_arcs) num_columns = len(self.__v_arcs) num_points = num_rows * num_columns root_node = -1 possible_points = set() neighbors = [[] for _ in range(num_points)] for i in range(num_rows): for j in range(num_columns - 1): h_arc = self.__h_arcs[i][j] if h_arc.Max() == 1: head = i * num_columns + j tail = i * num_columns + j + 1 neighbors[head].append(tail) neighbors[tail].append(head) possible_points.add(head) possible_points.add(tail) if root_node == -1 and h_arc.Min() == 1: root_node = head for i in range(num_rows - 1): for j in range(num_columns): v_arc = self.__v_arcs[j][i] if v_arc.Max() == 1: head = i * num_columns + j tail = (i + 1) * num_columns + j neighbors[head].append(tail) neighbors[tail].append(head) possible_points.add(head) possible_points.add(tail) if root_node == -1 and v_arc.Min() == 1: root_node = head if root_node == -1: return visited_points = set() to_process = deque() # Compute reachable points to_process.append(root_node) while to_process: candidate = to_process.popleft() visited_points.add(candidate) for neighbor in neighbors[candidate]: if not neighbor in visited_points: to_process.append(neighbor) visited_points.add(neighbor) if len(visited_points) < len(possible_points): for point in visited_points: possible_points.remove(point) # Loop on unreachable points and zero all neighboring arcs. for point in possible_points: i = point // num_columns j = point % num_columns neighbors = NeighboringArcs(i, j, self.__h_arcs, self.__v_arcs) for var in neighbors: var.SetMax(0) def SlitherLink(data): num_rows = len(data) num_columns = len(data[0]) solver = pywrapcp.Solver('slitherlink') h_arcs = [[ solver.BoolVar('h_arcs[%i][%i]' % (i, j)) for j in range(num_columns) ] for i in range(num_rows + 1)] v_arcs = [[ solver.BoolVar('v_arcs[%i][%i]' % (i, j)) for j in range(num_rows) ] for i in range(num_columns + 1)] # Constraint on the sum or arcs for i in range(num_rows): for j in range(num_columns): if data[i][j] != -1: sq = [h_arcs[i][j], h_arcs[i + 1][j], v_arcs[j][i], v_arcs[j + 1][i]] solver.Add(solver.SumEquality(sq, data[i][j])) # Single loop: each node has a degree 0 or 2 zero_or_two = [0, 2] for i in range(num_rows + 1): for j in range(num_columns + 1): neighbors = NeighboringArcs(i, j, h_arcs, v_arcs) solver.Add(solver.Sum(neighbors).Member(zero_or_two)) # Single loop: sum or arcs on row or column is even for i in range(num_columns): column = [h_arcs[j][i] for j in range(num_rows + 1)] solver.Add(BooleanSumEven(solver, column)) for i in range(num_rows): row = [v_arcs[j][i] for j in range(num_columns + 1)] solver.Add(BooleanSumEven(solver, row)) # Single loop: main constraint solver.Add(GridSinglePath(solver, h_arcs, v_arcs)) # Special rule on corners: value == 3 implies 2 border arcs used. if data[0][0] == 3: h_arcs[0][0].SetMin(1) v_arcs[0][0].SetMin(1) if data[0][num_columns - 1] == 3: h_arcs[0][num_columns - 1].SetMin(1) v_arcs[num_columns][0].SetMin(1) if data[num_rows - 1][0] == 3: h_arcs[num_rows][0].SetMin(1) v_arcs[0][num_rows - 1].SetMin(1) if data[num_rows - 1][num_columns - 1] == 3: h_arcs[num_rows][num_columns - 1].SetMin(1) v_arcs[num_columns][num_rows - 1].SetMin(1) # Search all_vars = [] for row in h_arcs: all_vars.extend(row) for column in v_arcs: all_vars.extend(column) db = solver.Phase(all_vars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE) log = solver.SearchLog(1000000) solver.NewSearch(db, log) while solver.NextSolution(): PrintSolution(data, h_arcs, v_arcs) solver.EndSearch() if __name__ == '__main__': SlitherLink(small) SlitherLink(medium) SlitherLink(big)