Search code examples
sympy

Solving a "coins" puzzle with SymPy (or other Python tool)?


A friend posed the following puzzle (from a real life experience). He had 17 coins which he exchanged for exactly three quarters (laundry day).

What could those coins have been?

Another friend solved this using the following Mathematica™:

Reduce[{
   25 q + 10 d + 5 n + p == 75,
   q + d + n + p == 17,
   q \[Element] Integers, d \[Element] Integers, 
   n \[Element] Integers, p \[Element] Integers,
   q >= 0, d >= 0, n >= 0, p >= 0
}]

... and I'd like to be able to solve it (well, problems of this sort) using Python's SymPy if possible, or some other tool like pyDatalog or kanren or whatever.

However, I don't know quite how to connect the dots. I got (even before seeing the Mathematica solution) that it's a system of two equations:

#!python
import sympy
from sympy import Symbol, Eq
from sympy.solvers import solve

d = Symbol('d', integer=True)  # Dimes
n = Symbol('n', integer=True)  # Nickels
p = Symbol('p', integer=True)  # Pennies
## Assuming he didn't trade quarters for more quarters
f1 = Eq(d + n + p, 17)           # Number of coins he had
f2 = Eq(d * 10 + n * 5 + p, 75)  # Total value of the coins

But I don't know how to go from there. When I call solve() on this I get:

#!python
solve((f1, f2), (d, n, p))
# ---> {n: -9*p/5 + 19, d: 4*p/5 - 2}

(For reference I know I should be getting a couple of different possible solutions: (d,n,p) => (2,10,5), (6,1,10) ... and there's another that's violating my constraint that he wouldn't exchange quarters for quarters).

I'm sure it's just that I don't understand the tool well enough.

So, how do I solve a system of equations in Sympy, with constraints that the results be non-negative integers? Alternatively how might I do this using a Python logical programming tool or framework?


Solution

  • The way so solve integer equations in sympy is with diophantine from sympy.solvers.diophantine.diophantine. It does not seem to support systems of equations but with a bit of substituting it can still be used:

    Frist I reduce the problem to one equation:

    term1 = d + n + p - 17
    term2 = term1.subs(*solve(f2))
    

    gives

    n/2 + 9*p/10 - 19/2
    

    But when I supply this to diophantine it complains it wants integer coefficients so I do

    def make_coefs_integers(term):
        factor = reduce(sympy.lcm, [x.q for x in term.as_coefficients_dict().values()])
        return term*factor
    
    term2 = make_coefs_integers(term2)
    

    giving me

    5*n + 9*p - 95
    

    For this diophantine gives me the solution but not quite in the form I need. It returns a set with tuples where the elements of the tuples are solutions for the free sympols in the equation. Since we only have one element in the set I can convert it to a dictionary similar to what solve returns by zipping the solution tuple with the free symbols. I like that form a lot since it can be given to subs.

    sol2 = dict(zip(sorted(term2.free_symbols, key=str), tuple(diophantine(make_coefs_integers(term2)))[0]))
    

    which is

    {n: 9*t_0 + 190, p: -5*t_0 - 95}
    

    To also get d we put it in to term1 but use t instead of t0 to avoid confusion:

    term3 = term1.subs(sol2).subs({t0:t})
    

    again this is:

    d + 4*t + 78
    

    Solving once more:

    sol3 = dict(zip(sorted(term3.free_symbols, key=str), tuple(diophantine(term3))[0]))
    

    gives

    {d: 4*t_0 - 78, t: -t_0}
    

    Which we can combine to a complete solution:

    sol4 = {k:v.subs({t0:-t0}) for k,v in sol2.items()} | {d:sol3[d]}
    

    which is

    {n: 190 - 9*t_0, p: 5*t_0 - 95, d: 4*t_0 - 78}
    

    And now we can even reproduce the mathematica solutions:

    {k:v.subs({tuple(v.free_symbols)[0]:20}) for k,v in sol4.items()}
    

    being

    {n: 10, p: 5, d: 2}
    

    and

    {k:v.subs({tuple(v.free_symbols)[0]:21}) for k,v in sol4.items()}
    

    being

    {n: 1, p: 10, d: 6}