Python函数式编程: 求解24点
引言
本文实现三种大同小异的基于“遍历+递归”的搜索,从一个侧面体现了函数式编程的妙处。
(所以,仅仅是简单的“遍历+递归”真的称得上是函数式编程么?捂脸笑)
如果只想看24点的解法,直接看版本2.
文章目录
求解思路
通过搜索解决问题。
- 设计
solve(xs, target)
函数,这个函数递归地验证列表xs
中的数通过加减乘除是否能获得target
值。函数的返回值如果为True
,说明最后能够计算成功。 - 遍历“执行一次四则运算”对应的子情况,对于每个情况,按照要求更新列表和
target
,把产生新列表xss
(通常变短一格)和target
(可能变也可能不变)投入solve
函数计算。- 子情况有多少种?根据子情况设计内容的不同,子情况的种数也不同。下面分了两种情况讨论。
- 当列表中只有一个数时,如果和
target
相等,则寻找成功,否则失败。
我们进一步地对求解的函数有这样的期许:
- 设计
solve_record(xs, target, records)
,前两个不变,最后records
保存已经规约的四则运算。- 已经规约的四则运算如何表示?在下1中用一个不断更新的字符串表示,下2中用一个不断追加的列表表示。
- 和
solve
一样,当寻找成功时返回,此时返回值为True
和完整的规约信息,从而能了解到完整的规约过程。
基础:加减乘除、等于的定义
def div(x, y):
'''
可以检查infty和0的除法函数。
这里的设计可能比较低效,但是不会影响整体速率,而且写起来很清楚。
'''
if x == 0:
if y != 0: return 0
else: return 1
elif x == float('inf'):
if y != float('inf'): return float('inf')
else: return 1
else:
if y == float('inf'): return 0
elif y == 0: return float('inf')
else: return x / y
def equal(a, b): return abs(a - b) < 1e-5
# equal函数的使用是为了应付浮点计算的误差。毕竟没有采用分数计算。
operators = [lambda x: lambda y: x+y, lambda x: lambda y: x-y, lambda x: lambda y: x*y, lambda x: lambda y: div(x, y)]
op_str = ["+", "-", "*", "/"]
# 用来遍历的运算列表。
版本0:阉割版,只能按照列表顺序加符号
版本0是一种阉割的24点,但是因为其实现比较经典,所以拉出来说。
只允许按照列表顺序加符号,比如1 2 1 7
可以(1+2)*(1+7)=24
,但是1 1 2 7
就不行。
思路:每次合并相邻的两个数。当列表只剩一个数,且与target
相等,则返回成功。
子情况种数:例如在列表长度为4时,选择两个相邻的数有3种情况,运算有4种情况,所以往下找一共有12种子情况。
设计实现:solve
函数在每一步进行遍历,先遍历数字再遍历运算,在遍历的每种情况,先进行这步计算操作,使相邻的两个数合并成一个,再通过子列表和target
往下递归。当列表只剩一个数,且与target
相等,则返回成功。
代码:缺
版本1:阉割版,只能串行
只允许从左到右地计算结果,如8+4/2+18=24
,从左向右算,无乘除优先级,无括号。
版本1其实和真正的24点游戏有差别,如(1+2)*(1+7)=24
不被认可,因为没有办法设计一个序列,从左到右计算24.
子情况种数:例如在列表长度为4时,选择最后一个操作数有4种情况,运算有4种情况,所以往下找一共有16种子情况。
设计实现:solve
函数在每一步进行遍历,先遍历数字再遍历运算,在遍历的每种情况,先还原这步计算操作,使target
恢复原值,再通过子列表和新target
往下递归。当列表只剩一个数,且与target
相等,则返回成功。
def solve(xs, target):
if len(xs) == 1:
return equal(xs[0], target)
else:
for i in range(len(xs)):
x, xss = xs[i], xs[:i]+xs[i+1:] # 移除特定位置的元素,而不影响原列表
targets = [op(target)(x) for op in operators]
for tar in targets:
if solve(xss, tar):
return True
return False
从solve
到solve_record
的进化:逻辑一样,但是信息多了一个record
.
因为规约的过程本质是从后往前构建串行计算顺序的过程,所以这里的record
是一个字符串,逐渐往前加符号+数字
。
def solve_record(xs, target, record):
if len(xs) == 1:
if equal(xs[0], target):
return (True, str(xs[0])+record)
else:
return (False, "")
else:
for i in range(len(xs)):
x, xss = xs[i], xs[:i]+xs[i+1:] # 移除特定位置的元素,而不影响原列表
targets = [operators[i](target)(x) for i in range(4)]
for j in range(4):
tar = targets[j]
result = solve_record(xss, tar, op_str[j]+str(x)+record)
if result[0]:
return result
return (False, "")
做一些测试。
print(solve([1,2,3], 1/6))
print(solve_record([1,2,3],1/6,""))
print(solve_record([1,2],1,""))
print(solve_record([1,2],2,""))
print(solve_record([1,2],0.5,""))
print(solve_record([5,5,5,1],24,""))
针对版本1做了简单的Haskell实现,只做了solve
:
import Data.List (delete)
operators :: Fractional b => [b -> b -> b]
operators = [(+), (-), (*), (/)]
and_ls :: Foldable t => t Bool -> Bool
and_ls = any (==True)
solve :: (Eq a, Fractional a) => [a] -> a -> Bool -- 可是Eq应该是包含在Fractional里面的,费解
solve [x] target = target == x
solve xs target = and_ls [and_ls [let xs' = delete x xs in solve xs' target' | target' <- [operator target x | operator <- operators]] | x <- xs]
如果要进一步做solve_record
的话,问题的类型框架大概是这样的:
type Infor = ([Fractional b => [b -> b -> b]], [Fractional c => [c])
solve_record :: (Eq a, Fractional a) => [a] -> a -> Infor -> (Bool, Infor)
定义了一个结构体来描述递归的返回值。
往后不会写了,先烂尾在这。
版本2:正确的实现:允许换序
版本2是正经的24点玩法:顺序完全随便,只要凑出来24点就行。
子情况种数:例如在列表长度为4时,两个数的组合(分先后)有12种情况,运算有4种情况,所以往下找一共有48种子情况(因为交换律而有重复,但大体如此)。
版本2的语法树表达能力更强,因为版本2允许的情况包括版本1所有的情况。换句话说,只要版本1中合法拼出24点的情况,在情况2都是合法的,反之不然。
设计实现:solve
函数在每一步进行遍历,先遍历二元数对再遍历运算,在遍历的每种情况,执行这步计算操作产生新的值,再通过新的列表和target
往下递归。当列表只剩一个数,且与target
相等,则返回成功。
def solve(xs, target):
if len(xs) == 1:
return equal(xs[0], target)
else:
for i in range(len(xs)):
for j in range(len(xs)):
if i == j:
continue
x_news = [op(xs[i])(xs[j]) for op in operators]
smaller = i if i < j else j
bigger = i + j - smaller
xss = xs[:bigger]+xs[bigger+1:]
for x_new in x_news:
xss[smaller] = x_new
if solve(xss, target):
print(xss)
return True
# else:
# print(xss, "Not Success")
return False
从solve
到solve_record
的进化:逻辑一样,但是信息多了一个record
.
因为规约的过程本质是数字合并的过程,所以这里的record
是一个列表,每次追加一个字符串数字<op>数字=数字
。
def solve_record(xs, target, records): # records: 一个字符串列表,用来存储各步计算结果
if len(xs) == 1:
if equal(xs[0], target):
return True, records
else:
return False, []
else:
for i in range(len(xs)):
for j in range(len(xs)):
if i == j:
continue
xi, xj = xs[i], xs[j]
smaller = i if i < j else j
bigger = i + j - smaller
xss = xs[:bigger]+xs[bigger+1:] # 移除特定位置的元素,而不影响原列表
for k in range(4):
x_new = operators[k](xi)(xj)
record_new = str(xi) + op_str[k] + str(xj) + "=" + str(x_new)
xss[smaller] = x_new
result = solve_record(xss, target, records+[record_new])
if result[0]:
return result
return False, []
封装一个24点函数并进行测试:
def solve_24(*args):
xs = list(args)
return solve_record(xs, 24, [])
print(solve_24(5,5,5,1))
print(solve_24(3,3,8,8))
print(solve_24(1,1,2,7))
print(solve_24(1,10,3,3))
结果很好。
(True, ['1/5=0.2', '5-0.2=4.8', '4.8*5=24.0'])
(True, ['8/3=2.6666666666666665', '3-2.6666666666666665=0.3333333333333335', '8/0.3333333333333335=23.99999999999999'])
(True, ['1+2=3', '1+7=8', '3*8=24'])
(True, ['1+10=11', '11-3=8', '8*3=24'])
Haskell实现(待补)
有点难,不太会写,待补。
Python的好处在于功能齐全且好写,Haskell的好处在于类型系统保证编程者不容易出错。