Alphabet mathematics

Alphabet Mathematics solution v1, Jacob Dlougach.

Winner

Very clean implementation, and the use of coroutines in order to generate permutations was a nice way of handling it.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
--[[
    Super-fast bruteforcing
    
    The description of the algorithm follows:
    1. For each alphanumeric operand (left, right and answer) build masks:
         For example, initialSmth = numericMask(Smth)
         numericMask(A42B90A) = 0420900 , that is only consider digits.
         Then calculate mask for each letter:
         letterMask(A42B90A, A) = 1000001
         letterMask(A42B90A, B) = 0001000
    2. Go through all possible assignments of letters
    3. For each assignment (A => x, B => y) you can get number value with
       the following formula:
           N = numericMask + x*letterMask(A) + y*letterMask(B)
    4. Check if those numbers are cool for us, and put them into the list if
       they fit into the equation.
    
    Why this actually works so well, or history of making this solution:
    
      First when I read the problem, first thing which came to my mind was:
      "Hey, 10! is less than 4000000, now what I am waiting for,
      it's simple bruteforcing!!!" Then I sat down and implemented that
      simple bruteforcing. I used gsub in cycle to replace letters with digits,
      and then checked if those numbers were good or not. Also I was very
      optimistic about performance that time, so I even implemented BigIntegers
      in LUA... It worked on the tests from problem statement, but it would
      take whole 15 seconds to handle a simple case with only five letters...
      Then I decided to see, how much it would work if exactly 10 distinct
      letters were present. After 2 minutes of waiting, I pressed Ctrl-C.
      Nobody in WoWAce would wait so long. This had to be optimized.
      
      I decided, that BigIntegers were not worth taking care of in this
      problem, so I got rid of them, hoping that there won't be test cases
      for big numbers. That still hasn't solved my problems. I didn't know,
      where the bottleneck was, so I started commenting out pieces of code
      to see, which parts needed optimization. Minutes later it was clear,
      that the part with replacing letters with digits somehow took the most
      time. Then I implemented my own function for replacing everything in
      a single pass. Now it looked much better, only had to wait 40 seconds
      to get full screen of results. Now parsing and replacing took almost
      equal amount of time, so even joining both those operations into one
      function wouldn't dramatically increase performance. I was stuck, and
      I even thought that it was impossible to do it better than in 20 seconds.
      I decided to talk to my good old friend, Ilya. He tested my program
      like I did - looked for slow places and read Lua documentation
      (he hadn't seen Lua at all before). Then he said "Vrotmnenogi (russian
      for f*ckmybrain) these aren't integers, these are doubles!!!". Then
      we decided, that accessing elements of arrays by index slows down
      everything way too much and it should be avoided at all costs. Also
      before going to bed Ilya enlighted me about masks. Now it was clear,
      that with these two improvements my program will fly like engineers
      on their boots.
      
      Indeed, next day, when I implemented all those improvements in calculate
      function it worked only 10 seconds in worst case. Wonderful!
           
  ]]

local ZERO_CHAR = string.byte("0");

function calculate(left, operation, right, answer)
    left = left:reverse()
    right = right:reverse()
    answer = answer:reverse()  
    
    local lettersMaskLeft = {}
    local lettersMaskRight = {}
    local lettersMaskAnswer = {}
    
    local initialLeft = 0
    local initialRight = 0
    local initialAnswer = 0
    
    local hasMet = {}
    for i = 1,#left do
        local c = left:byte(i)
        if ( (c >= ZERO_CHAR) and (c < ZERO_CHAR + 10)) then
            initialLeft = initialLeft + (10^(i-1)) * (c - ZERO_CHAR)
        else
            hasMet[c] = true
            lettersMaskLeft[c] = (lettersMaskLeft[c] or 0) + 10^(i-1)
        end
    end
    for i = 1,#right do
        local c = right:byte(i)
        if ( (c >= ZERO_CHAR) and (c < ZERO_CHAR + 10)) then
            initialRight = initialRight + (10^(i-1)) * (c - ZERO_CHAR)
        else
            hasMet[c] = true
            lettersMaskRight[c] = (lettersMaskRight[c] or 0) + 10^(i-1)
        end
    end
    for i = 1,#answer do
        local c = answer:byte(i)
        if ( (c >= ZERO_CHAR) and (c < ZERO_CHAR + 10)) then
            initialAnswer = initialAnswer + (10^(i-1)) * (c - ZERO_CHAR)
        else
            hasMet[c] = true
            lettersMaskAnswer[c] = (lettersMaskAnswer[c] or 0) + 10^(i-1)
        end
    end
    
    local minLeft  = 10 ^ (#left - 1)
    local minRight = 10 ^ (#right - 1)
    local minAnswer = 10 ^ (#answer - 1)
    
    local metLetters = {}
    for k,v in pairs(hasMet) do
        table.insert(metLetters, k)
    end   
    if (table.maxn(metLetters) > 10) then
        return -- Impossible to do the matching (Dirichlet's box principle)
    end
    while (table.maxn(metLetters) < 10) do
        table.insert(metLetters, 0) -- Filling metLetters with inexistent char to simplify permutations bruteforcing
    end
    table.sort(metLetters)
    
    local result = {}

    local doPlus  = (operation == "+") or (operation == "?")
    local doMinus = (operation == "-") or (operation == "?")
    local doMult  = (operation == "*") or (operation == "?")
    local doPow   = (operation == "^") or (operation == "?")

    local l, r, a
    for p in perm(metLetters) do
        l = initialLeft
        r = initialRight
        a = initialAnswer 
        for i,c in ipairs(p) do
            if (c ~= 0) then
                l = l + (lettersMaskLeft[c] or 0) * (i - 1)
                r = r + (lettersMaskRight[c] or 0) * (i - 1)
                a = a + (lettersMaskAnswer[c] or 0) * (i - 1)
            end    
        end    
    
        -- Make sure that none of the numbers start with a "0"
        if ( (l >= minLeft) and (r >= minRight) and (a >= minAnswer))  then
            if (doPlus) then
                if (l + r == a) then
                    table.insert(result, tostring(l) .. " + " .. tostring(r) .. " = " .. tostring(a))
                end
            end
            if (doMinus) then
                if (l - r == a) then
                    table.insert(result, tostring(l) .. " - " .. tostring(r) .. " = " .. tostring(a))
                end
            end
            if (doMult) then
                if (l * r == a) then
                    table.insert(result, tostring(l) .. " * " .. tostring(r) .. " = " .. tostring(a))
                end
            end
            if (doPow) then
                if (l ^ r == a) then
                    table.insert(result, tostring(l) .. " ^ " .. tostring(r) .. " = " .. tostring(a))
                end
            end
        end
    end
    table.sort(result)
    for i,lres in ipairs(result) do
        print(lres)
    end
end

function arrayReverse(a, first, last)
    local i = first
    local j = last
    while (i + 1 < j) do
        a[i],a[j] = a[j],a[i]
        i = i + 1
        j = j - 1
    end
end

function perm(a)
    local prev, i, n  -- avoid creating new variables each time
    
    n = table.getn(a)
    
    function permgen(a) -- using next_permutation approach
        while true do
            coroutine.yield(a)
            prev = a[1];
            i = nil
            for j,v in ipairs(a) do
                if (v > prev) then
                    i = j
                    prev = v
                    break
                end
                prev = v
            end
            if (i == nil) then 
                arrayReverse(a, 1, n)
                return
            end
            for j,v in ipairs(a) do
                if (v < prev) then
                    a[i],a[j] = a[j],a[i]
                    break
                end
            end
            arrayReverse(a, 1, i - 1)
        end
    end
    
    return coroutine.wrap(function() permgen(a) end)
end


-- Testing code begins here
-- calculate("ABCDEFGHI", "+", "IHGFEDCBA", "1JJJJJJJJJ")
-- calculate("ABCD", "+", "BDC", "EAEA")
-- calculate("AAAAAAAA", "+", "B", "C")

Facts

Date created
06 Jun 2009
Updated
08 Jun 2009

Contestant