Κυριακή 28 Απριλίου 2013

Python generator awesomeness: SEND+MORE=MONEY

Hello,

One awesome thing with python is its "yield" keyword and the notion of generators. It can be applied in order to program in the functional and logic paradigms.

For example, let's take the following problem:

You have the expression:
SEND + MORE = MONEY
where every letter corresponds to a distinct digit (0 to 9, except S and M that cannot be 0). Find the corresponding digits for each of the letters so that the above equation holds.

So we have the variables S,E,N,D,M,O,R,Y.
With the domains:

S,M = [1,..., 9] 
E,N,D,O,R,Y = [0,...,9]
And the constraints:
S,E,N,D,M,O,R,Y distinct. 
SEND + MORE = MONEY


A beautiful way to do this is with python generators. For example, this can be solved in a single python statement as such:

    solutionGen = (\
        (s,e,n,d,m,o,r,y)\
        for s in range(1,10)\
        for m in range(1,10)\
        for e in range(0,10)\
        for n in range(0,10)\
        for d in range(0,10)\
        for o in range(0,10)\
        for r in range(0,10)\
        for y in range(0,10)\
        if len(set([s,e,n,d,m,o,r,y])) != len([s,e,n,d,m,o,r,y]) and\
        s*1000 + e*100 + n*10 + d +\
        m*1000 + o*100 + r+10 + e ==\
        m*10000 + o+1000 + n*100 + e*10 + y)
Just like that. You can get a single solution to this problem to SingleSolution with:
SingleSolution = solution.next()
Or every possible solution in a list with:
Solutions = [i for i in solution]
Magic.

Of course the above is not really efficient. This gave me a runtime of 122s on my core 2 duo e8400.

What you can do to improve it a bit? Well, for starters, replace the domains ( i.e range(0,10) ) with two constants out of the loop, i.e. Dom1 = range(1,10) and Dom2=range(1,10) so that they are not computed at each iteration. Then, a solution is more probable to appear at a range of bigger integers (we want two 4-digit numbers added to give a 5-digit number) so we can reverse the way we search the domains. So let's replace the above with:
    Dom1 = range(9,0,-1)
    Dom2 = range(9,-1,-1)
This, will give me a runtime of 16s.

Can we do more (less)? Yes. If you watch carefully to the above snipplet, we test every possible assignment for a solution while we could reduce the search space by not testing values that could not appear in a solution in the first place. For example, when S gets a value of '3', E cannot appear with the same value in the solution. So, while we search for a solution we sould "propagate" the distinction constraint. Using the yield keyword we can write the above snipplet as:

def solution():
    Dom1 = range(9,0,-1)
    Dom2 = range(9,-1,-1)
    for s in Dom1:
        for m in Dom1:
            if m == s:
                continue
            for e in Dom2:
                if e in [s,m]:
                    continue
                for n in Dom2:
                    if n in [s,m,e]:
                        continue
                    for d in Dom2:
                        if d in [s,m,e,n]:
                            continue
                        for o in Dom2:
                            if o in [s,m,e,n,d]:
                                continue
                            for r in Dom2:
                                if r in [s,m,e,n,d,o]:
                                    continue
                                for y in Dom2:
                                    if y in [s,m,e,n,d,o,r]:
                                        continue
                                    if C2(s,e,n,d,m,o,r,y):
                                        yield (s,e,n,d,m,o,r,y)

 Which gave me a runtime of  0.56s.

Not bad.

Δεν υπάρχουν σχόλια: