Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

N-Queens Symmetry Breaking Google OR Tools

One of the samples for the Google or-tools is a solver for the n-queens problem. At the bottom it says that the implementation can be improved by adding symmetry breaking constraints to the constraint solver.

Looking around the internet, I found the symmetry breaking constraints for the n-queens problem, but I cannot for the life of me figure out how to convert those to constraints to python code that implements them.


EDIT: this was a bad question, let's update...

What have I tried?

Here is the setup from the first link above:

from ortools.constraint_solver import pywrapcp

N = 8
solver = pywrapcp.Solver("n-queens")
# Creates the variables.
# The array index is the column, and the value is the row.
queens = [solver.IntVar(0, N - 1, "x%i" % i) for i in range(N)]
# Creates the constraints.
# All rows must be different.
solver.Add(solver.AllDifferent(queens))
# All columns must be different because the indices of queens are all different.
# No two queens can be on the same diagonal.
solver.Add(solver.AllDifferent([queens[i] + i for i in range(N)]))
solver.Add(solver.AllDifferent([queens[i] - i for i in range(N)]))

# TODO: add symmetry breaking constraints

db = solver.Phase(queens, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE)
solver.NewSearch(db)
num_solutions = 0
while solver.NextSolution():
  num_solutions += 1
solver.EndSearch()
print()
print("Solutions found:", num_solutions)
print("Time:", solver.WallTime(), "ms")

I know I can implement simple constraints successfully. If I wanted to ensure the solution always has a queen in the first column on the first row, I could implement that like this:

solver.Add(queens[0] == 0)

The queens[0] variable represents the queens location in the first column and this constraint is only satisfied when the first column has a queen in the first row. This of course is not what I want to do however because it's possible that a solution does not include any corner cells.

The symmetry breaking constraints for the n-queens problem are found below. They are pulled directly from the link in the second paragraph.

n-queens symmetry breaking constraints

I understand how these constraints work. The idea is that you can apply this function to each cell on the n-queens board in order to transform the state into an equivalent state. One of these states will be the canonical representation of that state. This is used as a method to prune future processing by eliminating duplicate evaluations.

If I were just implementing this in an after the fact way, I would do exactly what I describe above, convert the state using each possible symmetry breaking function, calculate some sort of state hash (e.g. a string of the selected row in each column) and select the one that's the lowest for each proposed solution. Skip future processing on ones we have seen before.

My problem is that I don't know how to convert these transformations into constraints for the google or-tools constraint programming solver.

Let's take a look at the simplest one, d1(r[i] = j) => r[j] = i, reflection about the main diagonal. What I know is that the transformation needs to be applied to all cells, then compared against the current state in order to prevent one from being expanded. I don't understand enough about python to understand what kind of expression works here for the transformation, and I just can't figure out how to create the constraint that compares the transformation to the current state for this particular solver.

state = [queens[i].Value() for i in range(N)]
symX    = [state[N - (i + 1)] for i in range(N)]
symY    = [N - (state[i] + 1) for i in range(N)]
symD1   = [state.index(i) for i in range(N)]
symD2   = [N - (state.index(N-(i+1)) + 1) for i in range(N)]
symR90  = [N - (state.index(i) + 1) for i in range(N)]
symR180 = [N - (state[N-(i+1)] + 1) for i in range(N)]
symR270 = [state.index(N-(i+1)) for i in range(N)]
like image 337
Nick Larsen Avatar asked Dec 13 '16 22:12

Nick Larsen


1 Answers

I tried to use a custom DecisionBuilder to prune the search tree using the symmetries as new constraints, but I couldn't make it work.

Instead I had to use a SearchMonitor that captures the event of every solution and check if that solution is a symmetry of a previous one.

Here i add the code of the SearchMonitor, the capture of the solution overriding the "AcceptSolution" function, and the gen_symetries function to calculate and check all possible symmetries.

    class SearchMonitor(pywrapcp.SearchMonitor):
    def __init__(self, solver, q):
        pywrapcp.SearchMonitor.__init__(self, solver)
        self.q = q
        self.all_solutions = []
        self.unique_solutions = []
        self.n = len(self.q)

    def AcceptSolution(self):
        qval = [self.q[i].Value() for i in range(self.n)]
        self.all_solutions.append(qval)

        symmetries = [vv in self.unique_solutions for vv in gen_symmetries(self.n, qval)]

        if sum(symmetries) == 0:
            self.unique_solutions.append(qval)

        return False

def gen_symmetries(n, solution):
    symmetries = []

    #x(r[i]=j) → r[n−i+1]=j
    x = list(range(n))
    for index in range(n):
        x[n - 1 - index] = solution[index]

    symmetries.append(x)

    #y(r[i]=j) → r[i]=n−j+1
    y = list(range(n))
    for index in range(n):
        y[index] = (n - 1 - solution[index])

    symmetries.append(y)

    #d1(r[i]=j) → r[j]=i
    d1 = list(range(n))
    for index in range(n):
        d1[solution[index]] = index

    symmetries.append(d1)

    # d2(r[i]=j) → r[n−j+1]=n−i+1
    d2 = list(range(n))
    for index in range(n):
        d2[n - 1 - solution[index]] = (n - 1 - index)

    symmetries.append(d2)

    # r90(r[i]=j) → r[j] = n−i+1
    r90 = list(range(n))
    for index in range(n):
        r90[solution[index]] = (n - 1 - index)

    symmetries.append(r90)

    # r180(r[i]=j) → r[n−i+1]=n−j+1
    r180 = list(range(n))
    for index in range(n):
        r180[n - 1 - index] = (n - 1 - solution[index])

    symmetries.append(r180)

    # r270(r[i]=j) → r[n−j+1]=i
    r270 = list(range(n))
    for index in range(n):
        r270[n - 1 - solution[index]] = index

    symmetries.append(r270)

    return symmetries

Later you just have to add the monitor to your solver like this.

monitor = SearchMonitor(solver, queens)
solver.Solve(db, monitor)
solver.NewSearch(db)

And finally just printing all the unique solutions

print("Unique Solutions:", len(monitor.unique_solutions), monitor.unique_solutions)

You can see the full working example in the gist.

https://gist.github.com/carlgira/7a4e6cf0f7b7412762171015917bccb4

like image 114
Carlos Giraldo Avatar answered Oct 01 '22 23:10

Carlos Giraldo