Python函数式编程: 求解24点

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

solvesolve_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

solvesolve_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的好处在于类型系统保证编程者不容易出错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值