分享一个由 Raymond Hettinger 编写的字母算术解迷函数
什么是字母算术?或者可以被称为 Cryptarthms 或者 Alphametics,如下面这样的就是:
HAWAII + IDAHO + IOWA + OHIO == STATES
510199 + 98153 + 9301 + 3593 == 621246其字母与数字的对应关系为:
H = 5
A = 1
W = 0
I = 9
D = 8
O = 3
S = 6
T = 2
E = 4solve() 函数
import re
import itertools
def solve(puzzle):
words = re.findall('[A-Z]+', puzzle.upper())
unique_characters = set(''.join(words))
assert len(unique_characters) <= 10, 'Too many letters'
first_letters = {word[0] for word in words}
n = len(first_letters)
sorted_characters = ''.join(first_letters) +
''.join(unique_characters - first_letters)
characters = tuple(ord(c) for c in sorted_characters)
digits = tuple(ord(c) for c in '0123456789')
zero = digits[0]
for guess in itertools.permutations(digits, len(characters)):
if zero not in guess[:n]:
equation = puzzle.translate(dict(zip(characters, guess)))
if eval(equation):
return equation
if __name__ == '__main__':
import sys
for puzzle in sys.argv[1:]:
print(puzzle)
solution = solve(puzzle)
if solution:
print(solution)
solve() 函数详解
words = re.findall('[A-Z]+', puzzle.upper())
unique_characters = set(''.join(words))
获取到所有字母,再将其保存到一个 SET 中,这个是该程序要做的第一件事情,在这里我们就使用re 模块的 findall() 函数,它接受一个正则表达式和一个字符串作为参数,并且取出所有该字符串中出现该模式的地方并保存到一个列表中。
对于取出之后的列表,我们还不能接着下一步工作,因为我们需要知道每一个字母对该的数字是什么,而不是一个字符串对应什么数字,所以我们需要将所有字符串中出现的字母取出来,这个时候可以用到 set ,它可以去除重复的值,还使用了字符串的 join() 函数,它将多个字符串使用一个空字符连接到一起,由于在Python中,字符串就是一个字符的序列,所以它可以直接被用来作为 set() 的参数,我们最后得到的将是一个以字母为元素的 Set 实例。
>>> words = re.findall('[A-Z]+', 'SEND + MOre = Money'.upper())
>>> words
['SEND', 'MORE', 'MONEY']
>>> set(words)
{'MONEY', 'SEND', 'MORE'}
>>> unique_charactors = set(''.join(words))
>>> unique_charactors
{'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}
接下来我们需要做的是判断字母数量是否大于 10 个,这个原因是:每一个不同的字母都必须对应一个不同的数字,然后总共只有 0 – 9 这 10 个数字,所以,如果字母数大于 10 个的话,那肯定会有多余的字母没有数字与之对应,也就不可能形成字母算术了。
这里使用了一个 assert 语句,该语句后面可以跟任何全法的 Python 表达式,如果表达式的结果为 True,那么该 assert 语句就与普通的表达式一样,但是如果运行结果为 False 的话,该语句将会抛出AssertionError 异常,assert 语句后面还可以再添加一个异常说明的字符串,当抛出异常时,该字符串将一并返回:
>>> assert len(unique_charactors) <= 10
>>> assert len(unique_charactors) >= 10, 'Aha, AssertionError has been catched'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: Aha, AssertionError has been catched
上面产生错误的代码其实与下面这个代码是待价的:
if len(unique_charactors) < 10:
raise AssertionError('Aha, AssertionError has been catched')
再接下来,就是很有趣的东西了,这是一个排列组合的问题,先获得总计有多少个字母,然后将就可以知道,对多有多少种可能的结果了,因为每一个字母对应一个数字,所以,我们可以把问题科化为0-9十个数字有多少种 N 个值的排列方法,这个N 就是字母的个数,在这里我们使用了一个十分有趣的函数: itertools.permutations()
itertools.permutaions() 函数
先来看一段代码:
>>> for i in itertools.permutations('ABC',2): print(i)
...
('A', 'B')
('A', 'C')
('B', 'A')
('B', 'C')
('C', 'A')
('C', 'B')
>>> for i in itertools.permutations('ABC',3): print(i)
...
('A', 'B', 'C')
('A', 'C', 'B')
('B', 'A', 'C')
('B', 'C', 'A')
('C', 'A', 'B')
('C', 'B', 'A')
>>> for i in itertools.permutations('ABC',4): print(i)
...
从上面的代码中可以看到,该函数接受一个序列(可以是任何迭代器或者列表等数据)以及一个排序的元素数目的参数,比如上例中第一段代码,我们就是将’A’,‘B’,‘C’三个字母进行排列,排列仅仅只有两个元素,而且每一个元素不能相同,permutations 会将鳘次排列返回,这个要让我们自己去写排列的话,还真的不简单,不过 Python 已经帮我们把这些都得很好了。
在本文的代码中,将每一个排列进行测试,如果得到正确的结果,刚停止运行并返回结果。
把所有东西放在一起
总的来说: 这个程序通过暴力解决字母算术谜题, 也就是通过穷举所有可能的解法。为了达到目的,它
- 通过re.findall()函数找到谜题中的所有字母
- 使用集合和set()函数找到谜题出现的所有不同的字母
- 通过assert语句检查是否有超过10个的不同的字母 (意味着谜题无解)
- 通过一个生成器对象将字符转换成对应的ASCII码值
- 使用itertools.permutations()函数计算所有可能的解法
- 使用translate()字符串方法将所有可能的解转换成Python表达式
- 使用eval()函数通过求值Python 表达式来检验解法
- 返回第一个求值结果为True的解法