2 min read

Revisiting Send + More = Money

In my last post I attempted to solve the Send + More = Money Cryptarithmetic problem. I didn't quite understand how I could do this quickly using a better algorithm, so I tried to find a simple brute force solution. As it turns out, that solution is actually reasonably fast, and finds the answer reliably in about 15 seconds on my machine.

Of course, I don't like computer response times that are in the seconds rather than the sub-seconds range, so I had to find a better way to get the answer. I did some searching, and I saw a lot of references to various approaches. None of them seems to be appropriate for me as a suitable solution for encoding into APL, until I found a paper on using a genetic algorithm. That gave me some inspiration based on the Hill climbing algorithms that are used in things like the N-queens problem.

Basically, I used the fitness function from the genetic algorithm paper to do a greedy local search (hill climbing) algorithm; the approach actually matches pretty closely to the genetic algorithm, but the genetic algorithm uses parallelism and a number of other things, such as multiple agents and populations. Instead, I did a straightforward local search weighted on the fitness function. To deal with the possibility of local but not global maxima, I again borrowed from the genetic algorithm to choose any random swap of two assignments.

And without further ado, here's the algorithm in a simple APL program.

[1]  ⍝ Solve the SEND+MORE=MONEY Cryptarithmetic problem
[2]  ⍝ using local search and minimum conflict heuristics.
[3]  ⍝
[4]  ⍝ Create selection vectors for the elements
[5]  CHARS←'demnorsy'
[6]  SEND←CHARS⍳'send' ⋄ MORE←CHARS⍳'more' ⋄ MONEY←CHARS⍳'money'
[7]  MS←CHARS⍳'ms'
[8]  ⍝
[9]  ⍝ Choose a variable assignment X randomly to start with.
[10] X←(10?10)-⎕IO
[11] ⍝
[12] ⍝ Fix fitness, swap, and correctness functions
[13] ⊣⎕FX 'Z←CONFLICTS X' 'Z←(10⊥X[MONEY])-(10⊥X[MORE])+10⊥X[SEND]'
[14] ⊣⎕FX 'Z←I (X SWAP) J' 'Z←X' 'Z[I J]←Z[J I]'
[15] ⊣⎕FX 'Z←GOOD X' 'Z←0^.≠X[MS]'
[16] ⍝
[17] ⍝ Check for finished product
[18] LP:→((0^.≠X[MS])^0=CONFLICTS X)/XT
[19] ⍝
[20] ⍝ Compute the possible choices
[21] B←(GOOD¨B)/B←∪,(⍳10)∘.(X SWAP)⍳10
[22] ⍝
[23] ⍝ Take the swap that minimizes the conflicts
[24] OLD←X ⋄ X←⊃B[↑⍋|CONFLICTS¨B]
[25] →(OLD≢X)/LP ⋄ (I J)←(2?10) ⋄ X[I J]←X[J I] ⋄ →LP
[26] ⍝
[27] ⍝ We are done and complete
[28] XT:Z←2 8⍴'DEMNORSY',X[⍳8]

Now, the real question is, how fast does this thing go? Is it faster than my brute force algorithm? I mean, they are both basically search algorithms. The answer: Yes! On this one, the timings jump because of the randomization that I use, but consistently, I get the answer back in under 100ms, and often under 50ms. Not bad!