pythonconstraint-programmingpython-constraint

python-constraint not solving the n-queens puzzle


I'm using the python-constraint library to try to solve the n-queens problem (n queens are placed on an n-by-n board and they must be arranged in such a way that they don't menace each other)

My formulation is as such:

from constraint import *

n = 8
problem = Problem()
problem.addVariables(range(n),range(n))

for i in range(n):
  for j in range(i):
    problem.addConstraint(lambda a,b:a!=b,(i,j))
    problem.addConstraint(lambda a,b:abs(a-b)!=abs(i-j),(i,j))

However, when I try to get the solution with problem.getSolution() i just get None. What am I doing wrong?


Solution

  • Very classical error with generated lambda (nothing to do with constraints).

    Consider this code

    ll=[]
    for i in range(3):
        l=lambda: print("i=", i)
        ll.append(l)
    
    ll[0]()
    ll[1]()
    ll[2]()
    

    It doesn't display 0, 1, 2 as you may expect. It displays 2, 2, 2.

    Those 3 (distinct) functions all do the same thing: they display i value. But if ll[0], ll[1], ll[2] are indeed 3 distinct functions, there is still only one i. Sure, i value was 0 when you created ll[0]. But ll[0] job is to display i, not to display the value i had once upon a time, when ll[0] was created.

    There are several ways to force evaluation of i at that moment

    One could be

    ll=[]
    for i in range(3):
       (lambda i: ll.append(lambda: print("i=", i)))(i)
    ll[0]()
    ll[1]()
    ll[2]()
    

    This times display is 0, 1, 2. Because what does ll[0] is to display i. But this time i is not the (unique) counter of the for loop. i is the parameter of the called lambda. And there are as many of them as of call of that lambda (the lambda i:... function, not the one created inside it).

    A clearer (but longer) version of the same code would be

    ll=[]
    
    def createAndAddLambdaToLL(x):
        l=lambda: print("x=", x)
        ll.append(l)
    
    for i in range(3):
        createAndAddLambdaToLL(i)
    
    ll[0]()
    ll[1]()
    ll[2]()
    

    createAndAddLambdaToLL is the outer lambda of my previous code. Note that I've also renamed the parameter x, to make clear that it is not i that is printed (i, at the time of ll[?]() calls would be 2 for all of them), but it is x that is printed. And there are 3 different x, created when createAndAddLambdaToLL is called.

    Edit: I add Bakuriu's comment; another way indeed, is to add optional parameters:

    ll=[]
    for i in range(3):
        l=lambda i=i: print("i=", i)
        ll.append(l)
    ll[0]()
    ll[1]()
    ll[2]()
    

    does print 0,1,2

    Because there are 2 kind of i here. The i that is printed is not the for loop counter. It is the local parameter i (ll[0](12) prints 12). And there are 3 such i (one, local to each ll[...]). With default value being the value of the counter i when function was created.

    So, Bakuriu's solution is as convoluted as mine when it comes to understand why it is necessary and why it works. But the writing is way simpler, and almost natural.

    Back to your problem

    from constraint import *
    
    n = 8
    problem = Problem()
    problem.addVariables(range(n),range(n))
    
    def addConstraintDiag(i,j):
        problem.addConstraint(lambda a,b: abs(a-b)!=abs(i-j), (i,j))
    
    for i in range(n):
        for j in range(i):
            problem.addConstraint(lambda a,b:a!=b,(i,j))
            addConstraintDiag(i,j)
    
    print(problem.getSolutions())
    

    Note that the first constraint (no sharing of rows) is not a problem, because lambda a,b:a!=b does not rely on i and j (in fact, it is 28 times the same function. You could create it once for all outside the loop, and add it as a constraint 28 times. But point is, it doesn't matter. That function does not need its i and j to be all 28 combinations, since it doesn't even use i and j. The constraint itself does, but not the lambda.

    To play safe, nevertheless, you could just add the 2 constraints in a function addConstraints, without trying to think to hard whether you need a partial evaluation of i and j or not.

    Edit: or, using Bakuriu's commnt:

    from constraint import *
    
    n = 8
    problem = Problem()
    problem.addVariables(range(n),range(n))
    
    for i in range(n):
        for j in range(i):
            problem.addConstraint(lambda a,b:a!=b,(i,j))
            problem.addConstraint(lambda a,b,i=i,j=j: abs(a-b)!=abs(i-j), (i,j))
    print(problem.getSolutions())