我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第 3 篇文章,点击查看活动详情”
能让计算机干的活儿,我绝不动脑,这就是程序员的信条;
-- 程序员张三最后的倔强
最近有点睡不着,或许大概可能还可以叫做失眠,就是因为掘金的小游戏 数字谜题,初级难度竟然尴尬了。出于对 程序员张三倔强信条 的尊重,决定来 破破破解 这个游戏,让电脑做它最擅长的事情。
1. 问题抽象
玩两局,虽然没通过,却很容易总结出规律。已知算子,运算符以及计算结果,求解是否存在某种运算公式使等式成立,如果存在,运算公式是什么!
如上述截图,算子就是 [9, 6, 3, 11],运算符是 -/, 计算结果为 1。
2. 算法推演
计算机最擅长的就是循环,算法优化的精髓在于减少循环;
我们可以通过递归的方式,遍历所有可能性。具体的做法是,用一个列表存储所有的算子,一个列表存储所用到的运算符,每次从列表中取出两个数字,再取出一个运算符,用运算的结果和算子列表中的其他数字,组成新的算子,重复上述操作,直到算子列表中只有一个数字。此时,再判断该数字与运算结果是否相同。
需要注意的点:
- 除法运算的结果为浮点数,所以列表存储的数字也都要当做浮点数。最后一步判断两个浮点是否相等时,需要考虑到误差。
- 除法运算中,除数不能为 0,如果遇到除数为 0 则直接跳过。
3. 算法实现
虽然 PHP 是世界上最好的语言,但人生苦短,我用 Python
from typing import List
class NumGame(object):
EPSILON = 1e-6 # 误差
RESULTS = []
def __init__(self, result: int, operators: List[str]):
self.result = result # 运算结果
self.operators = operators
def helper(self, nums: List[float], chars: List[str]) -> None:
"""
递归运算
@params nums: 算子
@params chars: 运算公式
"""
# 递归到列表长度为 1,判断是否与计算结果相同
if len(nums) == 1:
if abs(nums[0] - self.result) < self.EPSILON:
self.RESULTS.append(f"{chars[0][1: -1]} = {self.result}") # 这里1和-1是为了去掉首尾的括号,使公式更美观
return
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
x, y = nums[i], nums[j] # 取两个不同的数字
char_x, char_y = chars[i], chars[j]
rest_nums = nums[:i] + nums[i + 1:j] + nums[j + 1:]
rest_chars = chars[:i] + chars[i + 1:j] + chars[j + 1:]
for operator in self.operators:
if operator == "+":
self.helper(rest_nums + [x + y], rest_chars + ['(' + char_x + '+' + char_y + ')'])
if operator == "-":
self.helper(rest_nums + [x - y], rest_chars + ['(' + char_x + '-' + char_y + ')'])
self.helper(rest_nums + [y - x], rest_chars + ['(' + char_y + '-' + char_x + ')'])
if operator == "*":
self.helper(rest_nums + [x * y], rest_chars + ['(' + char_x + '*' + char_y + ')'])
if operator == "/":
if y != 0:
self.helper(rest_nums + [x / y], rest_chars + ['(' + char_x + '/' + char_y + ')'])
if x != 0:
self.helper(rest_nums + [y / x], rest_chars + ['(' + char_y + '/' + char_x + ')'])
3. 优化改进
是不是有 "海岛奇兵" 抢地盘的感觉,不管你有没有,反正我有;
用上面的代码,轻松拿下 关卡 1,本以为可以就这么轻松愉快的玩耍啦,结果 关卡 2 提示说,还可以把数字合并到一起,而且这个合并很有意思。针对测试算子 [9, 6, 3, 11],可以去除两个或两个以上的数字,组成新的算子,比如取出 [6, 3] 这两个数字,可以组成 63 或 36,即我们的算子列表可以是 [9, 36, 11] 或 [9, 63, 11]。
如此这般,算子的可能性将大大增加,不过值得庆幸的是,计算机擅长计算;
from copy import deepcopy
from typing import List
import itertools
class NumGame(object):
... # 此处省略掉上面代码
@staticmethod
def diff(a: List[float], b: List[float]) -> List[float]:
# 两个列表的差值: 去掉 a 列表中 b 的值
# 考虑到数字可能重复,舍弃掉 set.difference
new_a = deepcopy(a)
for item in deepcopy(b):
new_a.remove(item)
return new_a
def permutations(self, nums: List[float]) -> List[List[float]]:
"""
计算数字列表的排列组合
@params nums: 数字列表
"""
results = []
for i in range(len(nums)):
tmp = []
if i < 2:
continue
for item in list(itertools.permutations(nums, i)):
tmp.append(item)
for item in set(tmp):
diff = self.diff(a=nums, b=item)
diff += [int(''.join(list(map(str, item))))]
results.append(diff)
return results
def run(self, nums: List[float]):
permutations = self.permutations(nums)
permutations.append(nums)
for item in permutations:
self.helper(item, [str(i) for i in item])
print(set(self.RESULTS))
实现逻辑很简单,通过 itertools.permutations 方法获得算子列表排列组合。需要注意如下几点:
- 获取算子排列组合后,需要去重,有可能得到重复结果;
- 数字列表组合时,先转换成字符串列表,列表字符串元素相加后,再强制转换成数字。
int(''.join(list(map(str, item))))这是中鸡贼的做法,当然你也可以按位乘10计算; - 计算两个列表差集,最简单的办法是 set(a).difference(set(b)),非常方便获得存在于 a 列表中却不存在于 b 列表的元素,但 python 的元组有去重功能。考虑到列表中可能存在重复数字,用笨办法循环
remove即可;
4. 测试结果
完整代码
from copy import deepcopy
from typing import List
import itertools
class NumGame(object):
EPSILON = 1e-6 # 误差
RESULTS = []
def __init__(self, result: int, operators: List[str]):
self.result = result # 运算结果
self.operators = operators
def helper(self, nums: List[float], chars: List[str]) -> None:
"""
递归运算
@params nums: 算子
@params chars: 运算公式
"""
# 递归到列表长度为 1,判断是否与计算结果相同
if len(nums) == 1:
if abs(nums[0] - self.result) < self.EPSILON:
self.RESULTS.append(f"{chars[0][1: -1]} = {self.result}") # 这里1和-1是为了去掉首尾的括号,使公式更美观
return
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
x, y = nums[i], nums[j] # 取两个不同的数字
char_x, char_y = chars[i], chars[j]
rest_nums = nums[:i] + nums[i + 1:j] + nums[j + 1:]
rest_chars = chars[:i] + chars[i + 1:j] + chars[j + 1:]
for operator in self.operators:
if operator == "+":
self.helper(rest_nums + [x + y], rest_chars + ['(' + char_x + '+' + char_y + ')'])
if operator == "-":
self.helper(rest_nums + [x - y], rest_chars + ['(' + char_x + '-' + char_y + ')'])
self.helper(rest_nums + [y - x], rest_chars + ['(' + char_y + '-' + char_x + ')'])
if operator == "*":
self.helper(rest_nums + [x * y], rest_chars + ['(' + char_x + '*' + char_y + ')'])
if operator == "/":
if y != 0:
self.helper(rest_nums + [x / y], rest_chars + ['(' + char_x + '/' + char_y + ')'])
if x != 0:
self.helper(rest_nums + [y / x], rest_chars + ['(' + char_y + '/' + char_x + ')'])
@staticmethod
def diff(a: List[float], b: List[float]) -> List[float]:
# 两个列表的差值: 去掉 a 列表中 b 的值
# 考虑到数字可能重复,舍弃掉 set.difference
new_a = deepcopy(a)
for item in deepcopy(b):
new_a.remove(item)
return new_a
def permutations(self, nums: List[float]) -> List[List[float]]:
"""
计算数字列表的排列组合
@params nums: 数字列表
"""
results = []
for i in range(len(nums)):
tmp = []
if i < 2:
continue
for item in list(itertools.permutations(nums, i)):
tmp.append(item)
for item in set(tmp):
diff = self.diff(a=nums, b=item)
diff += [int(''.join(list(map(str, item))))]
results.append(diff)
return results
def run(self, nums: List[float]):
permutations = self.permutations(nums)
permutations.append(nums)
for item in permutations:
self.helper(item, [str(i) for i in item])
print(set(self.RESULTS))
# 测试用例
game = NumGame(result=1, operators=list("-/"))
cards = [9, 6, 3, 11]
game.run(cards)
# 运算组合
6-(11-(9-3)) = 1
(9-(3-6))-11 = 1
(9-(11-6))-3 = 1
9-(3-(6-11)) = 1
(9-11)-(3-6) = 1
(6/(11-9))/3 = 1
(11-9)/(6/3) = 1
(6-(3-9))-11 = 1
3/(6/(11-9)) = 1
9-(11-(6-3)) = 1
9/(11-(6/3)) = 1
(9-3)-(11-6) = 1
6-(3-(9-11)) = 1
(6-11)-(3-9) = 1
(6-3)-(11-9) = 1
(11-(6/3))/9 = 1
(6-(11-9))-3 = 1
(6/3)/(11-9) = 1
5. 改进方向
从计算结果来看,虽然等式成立,但细心的你很快就发现,算子重复使用了。如题,我们只有两个减法运算符和一个除法,而算法给出的答案里,竟然出现了三个减法或两个除法的情况。
算法还有有待改进,接下来就留给各位掘友啦;
评论区回复时请折叠代码,避免扎眼球,Markdown 折叠代码方案参考如下
<details>
<summary>完整代码</summary>
<pre><code>
your hello world
</code></pre>
</details>
点赞,回复,关注,加好友等等,少侠来一个吧!