python求解算式迷cryptarithmetic
这周末两天都在听 Peter Norvig 在udacity的公开课《Design of Computer Programs》,,很有意思,很受启发。Peter Norvig一把年纪了(1956年生,今年56岁了),很有激情,非常佩服
庆幸大学时期便坚持完整读厚厚的英文原版图书,坚持听英文的公开课,使得听Peter Norvig视频,没有障碍。如此才能受教于Peter Norvig,虽仅视频观摩,仍觉三生有幸,受益匪浅。
在第二讲“Back of the Envelope”里面,他一步一步的设计程序,来解算式迷(cryptarithmetic),并优化性能。算式迷也是我小学时期做的题目,长大了,发现居然还能用计算机程序轻松的解决,觉得很有意思,记录下来,权当备忘:
什么是算式迷(cryptarithmetic)
例如:
图片来自维基百科 Verbal arithmetic
- 字母代表0-9之间的数字.
- 不同的字母代表不同的数字.
- 相同的字母代表相同的数字.
那么各个字母分别代表什么数字,才能使等式成立?
程序解法
程序列举出所有可能,挑出满足条件的,便是答案。问题变为:
-
如何列举出所有可能
-
如何判断满足条件
用排列列举出所有可能
itertools.permutations
可以列出排列,比如
# 1, 2, 3的全排列
list(itertools.permutations((1, 2, 3)))
=>
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
# 1, 2, 3的全排列,仅取前面2位
list(itertools.permutations((1, 2, 3), 2))
=>
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
def fill_in(formula):
# 找出所有的大写字母(替换为数字)
letters = ''.join(set([s for s in formula if s in string.uppercase]))
# 列出所有的可能
for i in itertools.permutations('0123456789', len(letters)):
trans = string.maketrans(letters, ''.join(i))
# 变换。 比如"A+A==B" => "2+2==3"
yield string.translate(formula, trans)
如何判断满足条件
eval
可以用来帮忙:
eval('123 + 345 == 567') => False
eval('123 + 345 == 468') => True
def valid(f):
"Return True iff the statement eval to True, eg: 1+1==2"
try:
return not re.search(r'\b0[0-9]', f) and eval(f) is True
except ArithmeticError:
return False
解决问题
def solve(formula):
return (f for f in fill_in(formula) if valid(f))
for s in solve('SEND+MORE==MONEY'):
print s
=> 9567+1085==10652
但解决的过程比较漫长,我的电脑需要需要花掉17.5s,才能找出解。profile发现eval花了大部分时间(14.3s):
python -m cProfile code.py
1814400 0.920 0.000 4.042 0.000 re.py:139(search)
1814400 1.136 0.000 1.472 0.000 re.py:226(_compile)
1451520 13.955 0.000 14.334 0.000 {eval}
优化性能
Peter Norvig说,优化它有两种做法:使eval变得更快,或者减少调用次数。他选择了第二种方式: eval一个string,需要经历parse,compile为bytecode,执行的三个阶段,前两个阶段是可以复用的: 比如可以把表达式变成下面的函数:
# SEND+MORE==MONEY
lambda E, D, M, O, N, S, R, Y: (1*D+10*N+100*E+1000*S)+(1*E+10*R+100*O+1000*M)==(1*Y+10*E+100*N+1000*O+10000*M)
# E, D, M, O, N, S, R, Y 是参数
变成函数的过程,可以用eval,并且仅仅调用一次,其它的仅是调用生成的函数
def compile_word(word):
if word.isupper():
terms = ['%d*%s' % (10**i, d)
for i, d in enumerate(word[::-1])]
return '(' + '+'.join(terms) + ')'
else:
return word
def compile_formula(formula, verbose=False):
letters = set(re.findall('[A-Z]', formula))
terms = re.split('([A-Z]+)', formula)
params = ', '.join(letters)
expr = ''.join(map(compile_word, terms))
f = 'lambda %s: %s' % (params, expr)
if verbose: print f
return eval(f), ''.join(letters)
def faster_solve(formula):
f, letters = compile_formula(formula, True)
for digits in itertools.permutations(range(10), len(letters)):
try:
if f(*digits) is True:
table = string.maketrans(letters, ''.join(map(str, digits)))
r = string.translate(formula, table)
# 数字的首位不能是0
if not re.search(r'\b0[0-9]', r):
yield r
except ArithmeticError:
pass
在我的机器上,faster_solve
仅需要1.2s,比未优化的solve
快10几倍
Google后发现,还有用prolog解决此问题的,速度比这个快上千倍(花的CPU时间在ms一下)Cryptarithmetic Puzzle Solver,以后有时间再研究一下,应该比较有意思。