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