examples/notebook/contrib/slitherlink.ipynb
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
First, you must install ortools package in this colab.
%pip install ortools
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()
SlitherLink(small)
SlitherLink(medium)
SlitherLink(big)