1
- from itertools import permutations , chain , product
2
1
"""
3
2
This solution will first parse the alphametic expression
4
3
grouping and counting letters buy digit ranks
10
9
to reduce the number of permutations
11
10
"""
12
11
12
+ from itertools import permutations , chain , product
13
+
13
14
14
- def digPerms ( digset , nzcharset , okzcharset ):
15
+ def dig_perms ( digit_set , non_zero_chars , ok_zero_chars ):
15
16
"""This function creates permutations given the set of digits,
16
17
letters not alllowed to be 0, and letters allowed to be 0
17
18
"""
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
22
23
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
26
27
# if either fewer digits than letters at all or fewer non-0 digits
27
28
# 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 :
29
30
return [] # Return no permutations possible
30
31
# Simple case when zeros are allowed everwhere
31
32
# 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 )
34
35
# 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 )
37
38
else :
38
39
# General case
39
40
# Generate a list of possible 0 positions
40
- poslst = list (range (nzcnt , totcnt ))
41
+ positions_list = list (range (non_zero_count , total_count ))
41
42
# Chain two iterators
42
43
# first iterator with all non-0 permutations
43
44
# second iterator with all permulations without 1 letter
44
45
# 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 )))
49
50
50
51
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 ):
52
53
"""This function recursively traces a parsed expression from lowest
53
54
digits to highest, generating additional digits when necessary
54
55
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):
60
61
# unique non-zero characters by rank
61
62
# unique zero-allowed characters by rank
62
63
# 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
64
65
# recursion cumulative parameters
65
66
# established characters with digits
66
67
# carry-over from the previous level
67
68
# remaining unassigned digits
68
- prevdict , cover , remdigs = tracecombo
69
+ prev_digits , carry_over , remaining_digits = trace_combo
69
70
# the maximal 10-power (beyond the maximal rank)
70
71
# is reached
71
- if p == maxp :
72
+ if power == max_digit_rank :
72
73
# Carry-over is zero, meaning solution is found
73
- if cover == 0 :
74
- return prevdict
74
+ if carry_over == 0 :
75
+ return prev_digits
75
76
else :
76
77
# Otherwise the solution in this branch is not found
77
78
# 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
82
83
# Break down the current level letter into what can be
83
84
# 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 ]
87
88
else :
88
- remexp .append ((c , v ))
89
+ remaining_exp .append ((caesar , van_gogh ))
89
90
# Generate permutations for the remaining digits and currecnt level
90
91
# 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 ]):
92
93
# build the dictionary for the new letters and this level
93
- newdict = dict (zip (diglets , newdigs ))
94
+ new_dict = dict (zip (digit_letters , newdigs ))
94
95
# 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 ])
97
98
# 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 :
100
101
# if divisible, update the dictionary to all established
101
- newdict .update (prevdict )
102
+ new_dict .update (prev_digits )
102
103
# proceed to the next level of recursion with
103
104
# the same eqparams, but updated digit dictionary,
104
105
# 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 )
108
109
# if the recursive call returned a non-empty dictionary
109
110
# this means the recursion has found a solution
110
111
# 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
113
114
# if no permutations are avaialble or no
114
115
# permutation gave the result return None
115
116
return None
116
117
117
118
118
- def solve (an ):
119
+ def solve (puzzle ):
119
120
"""A function to solve the alphametics problem
120
121
"""
121
122
# First, split the expresion into left and right parts by ==
122
123
# split each part into words by +
123
124
# strip spaces fro, each word, reverse each work to
124
125
# 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 ('==' )]
127
128
# Find the maximal lenght of the work, maximal possive digit rank or
128
129
# 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 ])
130
131
# Extract the leading letters for each (reversed) word
131
132
# 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 }
133
134
# 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 ())
142
143
# 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
150
151
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
152
153
# 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 ()):
155
156
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
157
158
# it does not impact the sum with 0-multiplier
158
159
# if the letter contributes to the sum
159
160
# and was not yet seen at lower ranks
160
- elif c not in totchars :
161
+ elif caesar not in total_chars :
161
162
# add the letter to either non-zero set
162
163
# 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 )
165
166
else :
166
- uokzchars [ p ].add (c )
167
+ zero_chars [ picasso ].add (caesar )
167
168
# add to the list as seen letter to ignore at the next
168
169
# ranks
169
- totchars .add (c )
170
+ total_chars .add (caesar )
170
171
# pre-build the combo list of letters for the rank
171
172
# 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 ]))
173
174
# pre-convert check dictionaries to tuples
174
- tchars [ p ] = tuple (chardict .items ())
175
+ multipliers_chars [ picasso ] = tuple (chardict .items ())
175
176
# 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