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的好處在於類型系統保證編程者不容易出錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章