Processing math: 100%

Sunday, February 21, 2021

Crossing the CrossProduct off my list

OK, so I know that I may be jumping the gun here since there is another week of CrossProduct yet to come, so I am sorry. SPOILER ALERT!!!! So, you know avert your eyes, if you want to keep solving CrossProduct the old fashioned way with pen and paper, which I definitely did for the first two weeks and even for a first stab at this week's. However, as fun as this logic puzzle has been, I figured that I could outsource the heavy lifting to a much better logician with more brain power ... so I built a generic solver.

The general gist of the solver is not too dissimilar from the way that I typically attacked the problem with pen and paper: use the prime factorizations of the inputs to recast the puzzle as an integer constraint satisfaction problem. Assume we have a CrossProduct puzzle with I rows and J columns.

For each element ni in the input column, we can write the unique prime factorization as ni=2Ai3Bi5Ci7Di,i=1,,I.

We know that there can be no higher factors by design since the puzzle is to write ni as the product of three single digit numbers. Similarly, for each element mj in the input row, we can write the unique prime factorization as mj=2αj3βj5γj7δj,j=1,,J.

Let uij,vij,wij,xij be nonnegative integers for i{1,2,,I},j{1,2,,J}. We want to model the entry in the ith row and jth column as fij=2uij3vij5wij7xij,

such that following product conditions hold Jj=1fij=ni,Ii=1fij=mj,i=1,,I,j=1,,J.

The condition that fij is a single digit number can be translated to the linear condition ln(2)uij+ln(3)vij+ln(5)wij+ln(7)xijln(9),i=1,,I,j=1,,J.

Similarly, the conditions that the products across rows and columns are equal to the values in the input column and row, respectively, are given by Jj=1uij=Ai,i=1,,I,Ii=1uij=αj,j=1,,JJj=1vij=Bi,i=1,,I,Ii=1vij=βj,j=1,,JJj=1wij=Ci,i=1,,I,Ii=1wij=γj,j=1,,JJj=1xij=Di,i=1,,I,Ii=1xij=δj,j=1,,J

Using the mip solver for Python, I set up in a small Jupyter notebook, which is pasted below. The solver is very quick, at least for the type of problem sizes that one could hope to solve by pen and paper. I haven't fully tested the bounds of its perfomance with either I or J get big, but presumably they would still be better than solving by hand... Again, last chance to avert your eyes!!!!

CrossProductSolver
In [1]:
import numpy as np
from mip import Model, xsum, INTEGER

def get_prime_factors(number, primes=[2, 3, 5, 7]):
    factors = ()
    for p in primes:
        k = 0
        while number % p == 0:
            k += 1
            number /= p
        factors += (k,)
    return factors

def solve_cross_product(column, row):
    n_rows = len(column)
    n_cols = len(row)
        
    crossProduct = Model()
    u = [[crossProduct.add_var('u({},{})'.format(i,j), var_type=INTEGER) for j in range(n_cols)] for i in range(n_rows)]
    v = [[crossProduct.add_var('v({},{})'.format(i,j), var_type=INTEGER) for j in range(n_cols)] for i in range(n_rows)]
    w = [[crossProduct.add_var('w({},{})'.format(i,j), var_type=INTEGER) for j in range(n_cols)] for i in range(n_rows)]
    x = [[crossProduct.add_var('x({},{})'.format(i,j), var_type=INTEGER) for j in range(n_cols)] for i in range(n_rows)]
    
    
    for i in range(n_rows):
        for j in range(n_cols):
            ## all values must be nonnegative integers
            crossProduct += u[i][j] >= 0
            crossProduct += v[i][j] >= 0
            crossProduct += w[i][j] >= 0
            crossProduct += x[i][j] >= 0
    
            ## all entries must yield single digit entries
            crossProduct += np.log(2.0) * u[i][j] + np.log(3.0) * v[i][j] + np.log(5.0) * w[i][j] + np.log(7.0) * x[i][j] <= np.log(9.0)
            
    ## each row must have the appropriate factors in order to multiply to the values in the input column
    for i, c_ in enumerate(column):
        factors = get_prime_factors(c_)
        crossProduct += xsum(u[i][j] for j in range(n_cols)) == factors[0]
        crossProduct += xsum(v[i][j] for j in range(n_cols)) == factors[1]
        crossProduct += xsum(w[i][j] for j in range(n_cols)) == factors[2]
        crossProduct += xsum(x[i][j] for j in range(n_cols)) == factors[3]
        
    ## each column must have the appropriate factors in order to multiply to the values in the input row
    for j, r_ in enumerate(row):
        factors = get_prime_factors(r_)
        crossProduct += xsum(u[i][j] for i in range(n_rows)) == factors[0]
        crossProduct += xsum(v[i][j] for i in range(n_rows)) == factors[1]
        crossProduct += xsum(w[i][j] for i in range(n_rows)) == factors[2]
        crossProduct += xsum(x[i][j] for i in range(n_rows)) == factors[3]
    
    status = crossProduct.optimize()
    if crossProduct.num_solutions:
        y = np.ones((n_rows, n_cols))
        for v in crossProduct.vars:
            name = v.name
            row_, col_ = v.name.split('(')[-1].split(')')[0].split(',')
            val = v.x
            y[int(row_),int(col_)] *= 2**val if name[0] == 'u' else 3**val if name[0] == 'v' else 5**val if name[0] == 'w' else 7**val
        print(y)
In [2]:
column = np.array([280, 168, 162, 360, 60, 256, 126])
row = np.array([183708, 245760, 117600])
solve_cross_product(column, row)
[[7. 8. 5.]
 [3. 8. 7.]
 [9. 6. 3.]
 [9. 8. 5.]
 [3. 5. 4.]
 [4. 8. 8.]
 [9. 2. 7.]]

No comments:

Post a Comment