Tchisla简介
最近玩到一个挺有意思的数字解密小游戏《Tchisla》,其规则类似算24点,也是利用一些数学运算和初始数字计算出目标数字,与算24点不同的是,Tchisla允许不限次数地使用一种初始数字(1~9),运算操作除了加减乘除外还包括了幂、平方根和阶乘,以及重复这个数字形成多位数(比如初始数字为7,那么777也可以使用)。游戏中的题目以“target # seed”的形式给出,我们的目标就是使用尽量少的初始数字seed加以各种计算得到目标数字target,比如题目11 # 7,最简解法就是11 = 77 / 7,比如题目5 # 9,最简解法为5 = √9 + (√9)! / √9。这里有篇文章更详细地介绍了这个游戏插电数字游戏——Tchisla 。
Tchisla求解器
这个游戏讲道理还是挺难的,尤其是挑战题目中的目标数都上千了,手动解题很难找到最优解,因此我花了半天用python写了个求解器,目前看起来效果不错,几十秒钟就帮我找到了了2016 # 1~9的所有最优解:
在求解Tchisla题目的过程中我相信是有一些内在的数学规律的,不过似乎很难总结出一套普适的最优求解算法,还是得暴力求解。暴力求解的思路其实挺简单的,从1个初始数字开始,找到所有合理的可达值及其表达式,放入一个列表中,这个称为第1代。后面再继续搜索第2代、第3代等等,对于第n代,其中的可达值就是n个初始数字可以表示的所有结果,计算第n代时我们可以根据前面的n-1代结果进行组合来生成新一代数据,比如5个初始数字可以得到的目标值就是第1代结果+第4代结果的各种算数组合加上第2代结果+第3代结果的各种算数组合。过程中一但发现目标值,我们就得到答案了。
虽然思路很简单,不过其中有些要注意的点,首先开方和阶乘运算这样的单目运算符不消耗初始数字,可以对一个表达式无限操作,为了避免无限搜索下去,所以第一点限制就是开方只对整数做、阶乘只对小于一定值的整数做。另外浮点数的精度丢失问题比较麻烦,需要合适的取整。此外,为了避免一些不太可能的无效搜索,合理的限制搜索数据的最大值也是必要的。
除了以上思路之外,为了得到结果,我们还需要一套表达式系统来跟踪我们的计算步骤,毕竟我们需要知道的最终是如何运算得到目标值而不仅仅是可以得到目标值的一个肯定。
以下放出代码,除了阶乘调用了系统库外,没有依赖别的库。代码前半部分是表达式系统,后半部分是求解器:
from math import factorial
class Expr:
threshold = 1e-10
def __init__(self, value):
if isinstance(value, float):
if value.is_integer():
value = int(value)
elif abs(value - round(value)) < Expr.threshold:
value = round(value)
self.value = value
def __str__(self):
raise TypeError("Should not call Expr.__str__()")
class LiteralExpr(Expr):
def __init__(self, value):
Expr.__init__(self, value)
self.literal = str(value)
def __str__(self):
return self.literal
class FactorialExpr(Expr):
def __init__(self, expr: Expr):
Expr.__init__(self, factorial(expr.value))
self.child = expr
def __str__(self):
if isinstance(self.child, BinaryExpr):
return '({})!'.format(str(self.child))
else:
return str(self.child) + '!'
class SqrtExpr(Expr):
def __init__(self, expr: Expr):
Expr.__init__(self, expr.value ** 0.5)
self.child = expr
def __str__(self):
if isinstance(self.child, LiteralExpr):
return '√{}'.format(str(self.child))
else:
return '√({})'.format(str(self.child))
class BinaryExpr(Expr):
def __init__(self, oper: str, left: Expr, right: Expr, value):
Expr.__init__(self, value)
self.oper = oper
self.left = left
self.right = right
def __str__(self):
format_str = '({})' if isinstance(self.left, BinaryExpr) else '{}'
format_str += ' {} '
format_str += '({})' if isinstance(self.right, BinaryExpr) else '{}'
return format_str.format(str(self.left), self.oper, str(self.right))
class AddExpr(BinaryExpr):
def __init__(self, left: Expr, right: Expr):
BinaryExpr.__init__(self, '+', left, right, left.value + right.value)
class SubExpr(BinaryExpr):
def __init__(self, left: Expr, right: Expr):
BinaryExpr.__init__(self, '-', left, right, left.value - right.value)
class MulExpr(BinaryExpr):
def __init__(self, left: Expr, right: Expr):
BinaryExpr.__init__(self, '*', left, right, left.value * right.value)
class DivExpr(BinaryExpr):
def __init__(self, left: Expr, right: Expr):
BinaryExpr.__init__(self, '/', left, right, left.value / right.value)
class PowExpr(BinaryExpr):
def __init__(self, left: Expr, right: Expr):
BinaryExpr.__init__(self, '^', left, right, left.value ** right.value)
class TchislaSolver:
value_limit = 10 ** 8
power_limit = 30
factorial_limit = 15
class Found(BaseException):
pass
def __init__(self, target: int, seed: int):
self.target = target
self.seed = seed
self.all_candidates = set()
self.current_candidates = []
self.generations = []
self.result = None
def solve(self, search_depth=10):
try:
while search_depth > 0:
begin, end = 0, len(self.generations) - 1
while begin <= end:
self.cross_candidates(self.generations[begin], self.generations[end])
begin += 1
end -= 1
self.add_literal(len(self.generations) + 1)
self.next_generation()
search_depth -= 1
except TchislaSolver.Found:
pass
return self.result
def cross_candidates(self, c1, c2):
for expr1 in c1:
for expr2 in c2:
self.add_addition(expr1, expr2)
self.add_subtraction(expr1, expr2)
self.add_multiplication(expr1, expr2)
self.add_division(expr1, expr2)
self.add_power(expr1, expr2)
def add_candidate(self, expr):
if expr.value == self.target:
self.result = expr
raise TchislaSolver.Found()
if expr.value > TchislaSolver.value_limit:
return
if expr.value not in self.all_candidates:
self.all_candidates.add(expr.value)
self.current_candidates.append(expr)
self.add_factorial(expr)
self.add_square_root(expr)
def next_generation(self):
self.generations.append(self.current_candidates)
self.current_candidates = []
def add_literal(self, repeats: int):
self.add_candidate(LiteralExpr(int(str(self.seed) * repeats)))
def add_addition(self, expr1: Expr, expr2: Expr):
self.add_candidate(AddExpr(expr1, expr2))
def add_subtraction(self, expr1: Expr, expr2: Expr):
if expr1.value > expr2.value:
self.add_candidate(SubExpr(expr1, expr2))
else:
self.add_candidate(SubExpr(expr2, expr1))
def add_multiplication(self, expr1: Expr, expr2: Expr):
self.add_candidate(MulExpr(expr1, expr2))
def add_division(self, expr1: Expr, expr2: Expr):
if expr1.value == 0 or expr2.value == 0:
return
self.add_candidate(DivExpr(expr1, expr2))
self.add_candidate(DivExpr(expr2, expr1))
def add_power(self, expr1: Expr, expr2: Expr):
if isinstance(expr2.value, int) and expr2.value <= TchislaSolver.power_limit:
self.add_candidate(PowExpr(expr1, expr2))
if isinstance(expr1.value, int) and expr1.value <= TchislaSolver.power_limit:
self.add_candidate(PowExpr(expr2, expr1))
def add_factorial(self, expr: Expr):
if isinstance(expr.value, int) and expr.value <= TchislaSolver.factorial_limit:
self.add_candidate(FactorialExpr(expr))
def add_square_root(self, expr: Expr):
if isinstance(expr.value, int) and expr.value > 0:
self.add_candidate(SqrtExpr(expr))
测试
这个求解器用起来很简单,可以编写一个简单的例子进行测试,计算2016 # 1~9的最优解:
for i in range(1, 10):
ts = TchislaSolver(2016, i)
print('2016 = ' + str(ts.solve()))
结果如下:
代码中对搜索和运算是有一些限制的,默认搜索10代,可达值最大只接受10^8,幂指数最大30,阶乘数最大15,这些限制可以加快搜索速度,一般是够用的,如果没搜索到最优解,也可以尝试修改以下值来调整并重新搜索:
value_limit = 10 ** 8
power_limit = 30
factorial_limit = 15
从上面的结果图里还可以发现表达式系统还有优化的空间,有些括号是多余的,不过懒得搞了,能用就行~