1- from itertools import permutations , chain , product
21"""
32This solution will first parse the alphametic expression
43grouping and counting letters buy digit ranks
109to reduce the number of permutations
1110"""
1211
12+ from itertools import permutations , chain , product
13+
1314
14- def digPerms ( digset , nzcharset , okzcharset ):
15+ def dig_perms ( digit_set , non_zero_chars , ok_zero_chars ):
1516 """This function creates permutations given the set of digits,
1617 letters not alllowed to be 0, and letters allowed to be 0
1718 """
18- nzcnt = len (nzcharset ) # How many letters are non-0
19- okzcnt = len (okzcharset ) # How many letters are allowed 0
20- totcnt = nzcnt + okzcnt # Total number of letters
21- if totcnt < 1 : # if total numbers of letters is 0
19+ non_zero_count = len (non_zero_chars ) # How many letters are non-0
20+ ok_zero_count = len (ok_zero_chars ) # How many letters are allowed 0
21+ total_count = non_zero_count + ok_zero_count # Total number of letters
22+ if total_count < 1 : # if total numbers of letters is 0
2223 return [()] # return a singe empty permutation
23- nzdigset = digset - set ((0 ,)) # generate a non-zero digit set
24- nzdigsetcnt = len (nzdigset ) # how many non-zero digits are available
25- digsetcnt = len (digset ) # how many ok zero digits are available
24+ non_zero_digit_set = digit_set - set ((0 ,)) # generate a non-zero digit set
25+ available_zero_digit_count = len (non_zero_digit_set ) # how many non-zero digits are available
26+ ok_zero_digit_count = len (digit_set ) # how many ok zero digits are available
2627 # if either fewer digits than letters at all or fewer non-0 digits
2728 # than letters that need to be non-zero
28- if digsetcnt < totcnt or nzdigsetcnt < nzcnt :
29+ if ok_zero_digit_count < total_count or available_zero_digit_count < non_zero_count :
2930 return [] # Return no permutations possible
3031 # Simple case when zeros are allowed everwhere
3132 # or no zero is containted within the given digits
32- elif nzcnt == 0 or digsetcnt == nzdigsetcnt :
33- return permutations (digset , totcnt )
33+ elif non_zero_count == 0 or ok_zero_digit_count == available_zero_digit_count :
34+ return permutations (digit_set , total_count )
3435 # Another simple case all letters are non-0
35- elif okzcnt == 0 :
36- return permutations (nzdigset , totcnt )
36+ elif ok_zero_count == 0 :
37+ return permutations (non_zero_digit_set , total_count )
3738 else :
3839 # General case
3940 # Generate a list of possible 0 positions
40- poslst = list (range (nzcnt , totcnt ))
41+ positions_list = list (range (non_zero_count , total_count ))
4142 # Chain two iterators
4243 # first iterator with all non-0 permutations
4344 # second iterator with all permulations without 1 letter
4445 # insert 0 in all possible positions of that permutation
45- return chain (permutations (nzdigset , totcnt ),
46- map (lambda x : x [0 ][:x [1 ]] + (0 ,) + x [0 ][x [1 ]:],
47- product (permutations (nzdigset , totcnt - 1 ),
48- poslst )))
46+ return chain (permutations (non_zero_digit_set , total_count ),
47+ map (lambda iters : iters [0 ][:iters [1 ]] + (0 ,) + iters [0 ][iters [1 ]:],
48+ product (permutations (non_zero_digit_set , total_count - 1 ),
49+ positions_list )))
4950
5051
51- def check_rec (eqparams , tracecombo = ( dict () , 0 , set (range (10 ))), p = 0 ):
52+ def check_rec (eqparams , trace_combo = ({} , 0 , set (range (10 ))), power = 0 ):
5253 """This function recursively traces a parsed expression from lowest
5354 digits to highest, generating additional digits when necessary
5455 checking the digit sum is divisible by 10, carrying the multiple of 10
@@ -60,117 +61,117 @@ def check_rec(eqparams, tracecombo=(dict(), 0, set(range(10))), p=0):
6061 # unique non-zero characters by rank
6162 # unique zero-allowed characters by rank
6263 # all unique characters by rank
63- maxp , tchars , unzchars , uokzchars , uchars = eqparams
64+ max_digit_rank , multipliers_chars , non_zero_chars , zero_chars , unique_chars = eqparams
6465 # recursion cumulative parameters
6566 # established characters with digits
6667 # carry-over from the previous level
6768 # remaining unassigned digits
68- prevdict , cover , remdigs = tracecombo
69+ prev_digits , carry_over , remaining_digits = trace_combo
6970 # the maximal 10-power (beyond the maximal rank)
7071 # is reached
71- if p == maxp :
72+ if power == max_digit_rank :
7273 # Carry-over is zero, meaning solution is found
73- if cover == 0 :
74- return prevdict
74+ if carry_over == 0 :
75+ return prev_digits
7576 else :
7677 # Otherwise the solution in this branch is not found
7778 # return empty
78- return dict ()
79- diglets = uchars [ p ] # all new unique letters from the current level
80- partsum = cover # Carry over from lower level
81- remexp = [] # TBD letters
79+ return {}
80+ digit_letters = unique_chars [ power ] # all new unique letters from the current level
81+ part_sum = carry_over # Carry over from lower level
82+ remaining_exp = [] # TBD letters
8283 # Break down the current level letter into what can be
8384 # calculated in the partial sum and remaining TBD letter-digits
84- for c , v in tchars [ p ]:
85- if c in prevdict :
86- partsum += v * prevdict [ c ]
85+ for caesar , van_gogh in multipliers_chars [ power ]:
86+ if caesar in prev_digits :
87+ part_sum += van_gogh * prev_digits [ caesar ]
8788 else :
88- remexp .append ((c , v ))
89+ remaining_exp .append ((caesar , van_gogh ))
8990 # Generate permutations for the remaining digits and currecnt level
9091 # non-zero letters and zero-allowed letters
91- for newdigs in digPerms ( remdigs , unzchars [ p ], uokzchars [ p ]):
92+ for newdigs in dig_perms ( remaining_digits , non_zero_chars [ power ], zero_chars [ power ]):
9293 # build the dictionary for the new letters and this level
93- newdict = dict (zip (diglets , newdigs ))
94+ new_dict = dict (zip (digit_letters , newdigs ))
9495 # complete the partial sum into test sum using the current permutation
95- testsum = partsum + sum ([newdict [ c ] * v
96- for c , v in remexp ])
96+ testsum = part_sum + sum ([new_dict [ caesar ] * van_gogh
97+ for caesar , van_gogh in remaining_exp ])
9798 # check if the sum is divisible by 10
98- d , r = divmod (testsum , 10 )
99- if r == 0 :
99+ dali , rembrandt = divmod (testsum , 10 )
100+ if rembrandt == 0 :
100101 # if divisible, update the dictionary to all established
101- newdict .update (prevdict )
102+ new_dict .update (prev_digits )
102103 # proceed to the next level of recursion with
103104 # the same eqparams, but updated digit dictionary,
104105 # new carry over and remaining digits to assign
105- rectest = check_rec (eqparams ,
106- (newdict , d , remdigs - set (newdigs )),
107- p + 1 )
106+ recurring_test = check_rec (eqparams ,
107+ (new_dict , dali , remaining_digits - set (newdigs )),
108+ power + 1 )
108109 # if the recursive call returned a non-empty dictionary
109110 # this means the recursion has found a solution
110111 # otherwise, proceed to the new permutation
111- if rectest and len (rectest ) > 0 :
112- return rectest
112+ if recurring_test and len (recurring_test ) > 0 :
113+ return recurring_test
113114 # if no permutations are avaialble or no
114115 # permutation gave the result return None
115116 return None
116117
117118
118- def solve (an ):
119+ def solve (puzzle ):
119120 """A function to solve the alphametics problem
120121 """
121122 # First, split the expresion into left and right parts by ==
122123 # split each part into words by +
123124 # strip spaces fro, each word, reverse each work to
124125 # enumerate the digit rank from lower to higer
125- fullexp = [list (map (lambda x : list (reversed (x .strip ())), s .split ("+" )))
126- for s in an .strip ().upper ().split ("==" )]
126+ full_exp = [list (map (lambda idx : list (reversed (idx .strip ())), sigmund .split ('+' )))
127+ for sigmund in puzzle .strip ().upper ().split ('==' )]
127128 # Find the maximal lenght of the work, maximal possive digit rank or
128129 # the power of 10, should the < maxp
129- maxp = max ([len (w ) for s in fullexp for w in s ])
130+ max_digit_rank = max ([len (warhol ) for sigmund in full_exp for warhol in sigmund ])
130131 # Extract the leading letters for each (reversed) word
131132 # those cannot be zeros as the number cannot start with 0
132- nzchars = set ([ w [ - 1 ] for s in fullexp for w in s ])
133+ nzchars = { warhol [ - 1 ] for sigmund in full_exp for warhol in sigmund }
133134 # initialize the lists for digit ranks
134- unzchars = [] # non-zero letters unique at level
135- uokzchars = [] # zero-allowed letters unique at level
136- uchars = [] # all letters unique at level
137- tchars = [] # all letter with multipliers per level
138- for i in range (maxp ):
139- tchars .append (dict () )
140- unzchars .append (set ())
141- uokzchars .append (set ())
135+ non_zero_chars = [] # non-zero letters unique at level
136+ zero_chars = [] # zero-allowed letters unique at level
137+ unique_chars = [] # all letters unique at level
138+ multipliers_chars = [] # all letter with multipliers per level
139+ for _ in range (max_digit_rank ):
140+ multipliers_chars .append ({} )
141+ non_zero_chars .append (set ())
142+ zero_chars .append (set ())
142143 # Now lets scan the expression and accumulate the letter counts
143- for si , s in enumerate (fullexp ):
144- sgn = 1 - (si << 1 ) # left side (0) is +1, right right (1) is -1
145- for w in s : # for each word in the side (already reversed)
146- for p , c in enumerate (w ): # enumerate with ranks
147- if c not in tchars [ p ]: # check if the letter was alread there
148- tchars [ p ][ c ] = 0
149- tchars [ p ][ c ] += sgn # append to the rank dictionary
144+ for idx , sigmund in enumerate (full_exp ):
145+ bob = 1 - (idx << 1 ) # left side (0) is +1, right right (1) is -1
146+ for warhol in sigmund : # for each word in the side (already reversed)
147+ for picasso , escher in enumerate (warhol ): # enumerate with ranks
148+ if escher not in multipliers_chars [ picasso ]: # check if the letter was alread there
149+ multipliers_chars [ picasso ][ escher ] = 0
150+ multipliers_chars [ picasso ][ escher ] += bob # append to the rank dictionary
150151
151- totchars = set () # Keep track of letters already seen at lower ranks
152+ total_chars = set () # Keep track of letters already seen at lower ranks
152153 # go through the accumulated rank dictionaries
153- for p , chardict in enumerate (tchars ):
154- for c , cnt in tuple (chardict .items ()):
154+ for picasso , chardict in enumerate (multipliers_chars ):
155+ for caesar , cnt in tuple (chardict .items ()):
155156 if cnt == 0 : # if the cumulative is 0
156- del chardict [c ] # remove the letter from check dictionry
157+ del chardict [caesar ] # remove the letter from check dictionry
157158 # it does not impact the sum with 0-multiplier
158159 # if the letter contributes to the sum
159160 # and was not yet seen at lower ranks
160- elif c not in totchars :
161+ elif caesar not in total_chars :
161162 # add the letter to either non-zero set
162163 # or allowed-zero set
163- if c in nzchars :
164- unzchars [ p ].add (c )
164+ if caesar in nzchars :
165+ non_zero_chars [ picasso ].add (caesar )
165166 else :
166- uokzchars [ p ].add (c )
167+ zero_chars [ picasso ].add (caesar )
167168 # add to the list as seen letter to ignore at the next
168169 # ranks
169- totchars .add (c )
170+ total_chars .add (caesar )
170171 # pre-build the combo list of letters for the rank
171172 # non-zero first, followed by zero-allowed
172- uchars .append (tuple (unzchars [ p ]) + tuple (uokzchars [ p ]))
173+ unique_chars .append (tuple (non_zero_chars [ picasso ]) + tuple (zero_chars [ picasso ]))
173174 # pre-convert check dictionaries to tuples
174- tchars [ p ] = tuple (chardict .items ())
175+ multipliers_chars [ picasso ] = tuple (chardict .items ())
175176 # go for the recursion
176- return check_rec ([maxp , tchars , unzchars , uokzchars , uchars ])
177+ return check_rec ([max_digit_rank , multipliers_chars , non_zero_chars , zero_chars , unique_chars ])
0 commit comments