海龜繪圖案例分析之移動火柴變等式游戲
說在前面
移動一根火柴棒使等式成立,是一種簡單有趣的智力游戲,只需具備簡單的算術(shù)知識,就能參與游戲,老少皆宜,深受大家的喜愛。前幾天,微信群里有老師聊起了這個游戲,毛毛老師還編寫了創(chuàng)建題庫的程序,好評如潮。在毛毛老師創(chuàng)意的啟發(fā)下,我對代碼做了進(jìn)一步改進(jìn),并利用海龜繪圖的鼠標(biāo)響應(yīng)功能,制作了可以移動火柴棒的小游戲。
由于該小游戲涉及功能較多,總計400多行代碼(含注釋),文章篇幅較大,我把它分成了三個部分,力求解析到位。今天和大家分享第一部分——創(chuàng)建題庫。

只允許移動一根火柴棒使等式成立。移動火柴棒的方法有三種:
一是在數(shù)字內(nèi)部移動火柴棒。例如原始等式為9-5=1,則可移動數(shù)字9的一根火柴棒,使其變成6,從而獲得正確等式6-5=1。
二是在數(shù)字之間移動火柴棒。例如原始等式為5-7=5,則可將數(shù)字7的一根火柴棒移到左邊數(shù)字5上,使得7變成1,5變成6,從而獲得正確等式6-1=5。
三是在數(shù)字和運算符之間移動火柴棒。例如原始等式為5+3=3,則可將運算符的一根火柴棒移到數(shù)字5上,使得+變成-,5變成6,從而獲得正確等式6-3=3。
根據(jù)上述移動火柴棒的方法,我們可以創(chuàng)建3個字典分別用來存儲移動、增加或減少火柴棒后,各數(shù)字可能變成的新數(shù)字。
例如,我們創(chuàng)建字典keep_dic用來存儲在數(shù)字內(nèi)部移動火柴棒后,各數(shù)字可能變成的新數(shù)字。如下圖所示,數(shù)字2可以變成數(shù)字3,我們記作keep_dic[2]=[3];數(shù)字3可以變成數(shù)字2或5,我們記作keep_dic[3]=[2,5];同理keep_dic[5]=[3],keep_dic[6]=[0,9]。

因為數(shù)字1不能變成任何數(shù)字,故我們記作keep_dic[1]=[]。根據(jù)上述規(guī)則,我們定義keep_dic ={1:[],2:[3],3:[2,5],4:[],5:[3],6:[0,9],7:[],8:[],9:[0,6],0:[6,9]}。
同理,我們創(chuàng)建字典add_dic用來存儲為數(shù)字增加一根火柴棒后,各數(shù)字可能變成的新數(shù)字。如下圖所示,數(shù)字1增加一根火柴后可以變成數(shù)字2,數(shù)字5增加一根火柴后可以變成數(shù)字6或9。

由此我們定義add_dic ={1:[7],2:[],3:[9],4:[],5:[6,9],6:[8],7:[],8:[],9:[8],0:[8]}。
同理,我們定義dec_dic ={1:[],2:[],3:[],4:[],5:[],6:[5],7:[1],8:[0,6,9],9:[3,5],0:[]},用來存儲為數(shù)字減少一根火柴棒后,各數(shù)字可能變成的新數(shù)字。
另外,我們還創(chuàng)建了兩個空列表ques和anss,分別用來存儲有效問題和對應(yīng)答案。
程序采用枚舉算法,采用四重循環(huán),依次枚舉運算符和3個運算數(shù)的值。
自定義函數(shù)get_question_bank(ques,anss)用來創(chuàng)建題庫,它通過枚舉不同運算符和操作數(shù)尋找所有可能的原始算術(shù)和答案。程序巧妙使用f-string來生成緊湊的字符串,并利用eval()函數(shù)計算出字符串表達(dá)式的結(jié)果,生成通用的關(guān)系表達(dá)式,以便篩選出等號不成立的原始算式。參考代碼如下:
'''函數(shù)功能:通過枚舉不同運算符和操作數(shù)尋找所有可能的原始算式和答案函數(shù)名:get_question_bank(ques,anss)參數(shù)表:ques -- 用來存儲問題(原始算式)的列表;sign -- 用來存儲答案(正確算式)的列表,每個問題可能有多個答案。返回值:沒有返回值。'''def get_question_bank(ques,anss): #獲取題庫(包括問題和答案)for sign in ['+','-']:for a in range(10):for b in range(10):for c in range(10):if eval(f'{a}{sign}') !=c: #只考慮等號不成立的原始算式ans = fun(a, b, c,sign) #調(diào)用尋找答案的驅(qū)動函數(shù),返回答案if ans != []: #若問題有解,則存儲問題和答案ques.append(f'{a}{sign}={c}') #存儲問題(原始算式)anss.append(ans) #存儲答案(正確算式)
知識小貼士
eval()函數(shù)用于返回傳入字符串的表達(dá)式的結(jié)果,還可以實現(xiàn)list、dict、tuple與str之間的轉(zhuǎn)化。
其語法格式為:eval(expression[,globals[, locals]])
參數(shù)說明:
expression :表達(dá)式。
globals :(可選參數(shù))變量作用域,全局命名空間,如果被提供,則必須是一個字典對象。
locals :(可選參數(shù))變量作用域,局部命名空間,如果被提供,可以是任何映射對象。
當(dāng)后兩個參數(shù)都為空時,等價于eval(expression),就是計算表達(dá)式的值。例如eval('pow(2,3)')的值為8,eval('2+3<5')的值為False;當(dāng)x=3時,eval(f'x+5*4')的值為23,但是eval(f'{x+5}*4')的值為32,因為把花括號內(nèi)部的表達(dá)式看作一個整體。
當(dāng)locals參數(shù)為空,globals參數(shù)不為空時,先查找globals參數(shù)中是否存在變量,并計算。例如eval("{'name':'Lucy','age':age}",{"age":18})的值為字典{'name': 'Lucy', 'age': 18}。
當(dāng)兩個參數(shù)都不為空時,先查找locals參數(shù),再查找globals參數(shù)。
eval()與input()函數(shù)配合,可以方便地實現(xiàn)表達(dá)式的還原與計算,例如執(zhí)行語句>>>eval(input("輸入一個表達(dá)式:")),當(dāng)用戶輸入字符串"3+2*6"時,系統(tǒng)輸出15。
但要注意其帶來的安全問題,因為被執(zhí)行的字符串表達(dá)式可能是某個系統(tǒng)命令。例如執(zhí)行語句>>>eval(input("輸入一個表達(dá)式:")),當(dāng)用戶惡意輸入字符串"__import__('os').system('dir')"時,會顯示當(dāng)前目錄文件。
4.驅(qū)動函數(shù)
如上所述,有三種移動火柴棒的方法。對應(yīng)每一種方法,我們都可以設(shè)置一個自定義函數(shù)來枚舉可能的答案。為了體現(xiàn)模塊化編程思想,我們設(shè)置一個驅(qū)動函數(shù)fun(),依次調(diào)用這三個自定義函數(shù),把所有可能的答案統(tǒng)一存儲到列表ans中。參考代碼如下:
'''函數(shù)功能:依次調(diào)用fun_1,fun_2,fun_3,為原始算術(shù)尋找可能的答案函數(shù)名:fun(a, b, c, sign)參數(shù)表:a,b,c -- 依次表示從左到右的3個操作數(shù)(單個數(shù)字);sign -- 運算符(加或減)。返回值:返回存儲了所有答案的列表ans,列表的每個元素都是一個表示答案的字符串。'''def fun(a, b, c, sign): #驅(qū)動函數(shù)ans = []fun_1(ans, a, b, c, sign) #數(shù)字內(nèi)部移動火柴棒fun_2(ans, a, b, c, sign) #數(shù)字之間移動火柴棒fun_3(ans, a, b, c, sign) #數(shù)字和運算符之間移動火柴棒return ans
5.功能模塊
接下來依次給出三種方法對應(yīng)的自定義函數(shù)。
'''函數(shù)功能:利用字典keep_dic,通過在數(shù)字內(nèi)部移動火柴棒來改變該數(shù)字函數(shù)名:fun_1(ans, a, b, c,sign)參數(shù)表:ans -- 列表的值為一個字符串,表示一個答案(正確算式);a,b,c -- 依次表示從左到右的3個操作數(shù)(單個數(shù)字);sign -- 運算符(加或減)。返回值:沒有返回值。'''def fun_1(ans, a, b, c,sign): #數(shù)字內(nèi)部移動火柴棒for i in keep_dic[a]: #在數(shù)字a內(nèi)部移動火柴棒if eval(f'{i}{sign}') == c:ans.append(f'{i}{sign}={c}')for i in keep_dic[b]: #在數(shù)字b內(nèi)部移動火柴棒if eval(f'{a}{sign}{i}') == c:ans.append(f'{a}{sign}{i}={c}')for i in keep_dic[c]: #在數(shù)字c內(nèi)部移動火柴棒if eval(f'{a}{sign}') == i:ans.append(f'{a}{sign}={i}')'''函數(shù)功能:利用字典dec_dic和add_dic,通過在數(shù)字之間移動火柴棒同時改變2個數(shù)字函數(shù)名:fun_2(ans, a, b, c,sign)參數(shù)表:ans -- 列表的值為一個字符串,表示一個答案(正確算式);a,b,c -- 依次表示從左到右的3個操作數(shù)(單個數(shù)字);sign -- 運算符(加或減)。返回值:沒有返回值。'''def fun_2(ans, a, b, c,sign): #數(shù)字之間移動火柴棒for i in dec_dic[a]: #從a中拿一根火柴到b中for j in add_dic[b]:if eval(f'{i}{sign}{j}') == c:ans.append(f'{i}{sign}{j}={c}')for i in dec_dic[a]: #從a中拿一根火柴到c中for j in add_dic[c]:if eval(f'{i}{sign}') == j:ans.append(f'{i}{sign}={j}')for i in dec_dic[b]: #從b中拿一根火柴到a中for j in add_dic[a]:if eval(f'{j}{sign}{i}') == c:ans.append(f'{j}{sign}{i}={c}')flag = Truefor i in dec_dic[b]: #從b中拿一根火柴到c中for j in add_dic[c]:if eval(f'{a}{sign}{i}') == j:ans.append(f'{a}{sign}{i}={j}')for i in dec_dic[c]: #從c中拿一根火柴到a中for j in add_dic[a]:if eval(f'{j}{sign}') == i:ans.append(f'{j}{sign}={i}')for i in dec_dic[c]: #從c中拿一根火柴到b中for j in add_dic[b]:if eval(f'{a}{sign}{j}') == i:ans.append(f'{a}{sign}{j}={i}')'''函數(shù)功能:利用字典dec_dic或add_dic,通過在數(shù)字和運算符之間移動火柴棒同時改變1個數(shù)字和運算符函數(shù)名:fun_3(ans, a, b, c,sign)參數(shù)表:ans -- 列表的值為一個字符串,表示一個答案(正確算式);a,b,c -- 依次表示從左到右的3個操作數(shù)(單個數(shù)字);sign -- 運算符(加或減)。返回值:沒有返回值。'''def fun_3(ans, a, b, c,sign): #數(shù)字和運算符之間移動火柴棒if sign == '+': #從運算符中拿一根火柴到數(shù)字sign, dic = '-', add_dicelse:sign, dic = '+', dec_dicfor i in dic[a]: #在運算符與數(shù)字a間移動火柴if eval(f'{i}{sign}') == c:ans.append(f'{i}{sign}={c}')for i in dic[b]: #在運算符與數(shù)字b間移動火柴if eval(f'{a}{sign}{i}') == c:ans.append(f'{a}{sign}{i}={c}')for i in dic[c]: #在運算符與數(shù)字c間移動火柴if eval(f'{a}{sign}') == i:????????????ans.append(f'{a}{sign}={i}')
代碼說明:
函數(shù)fun_1()利用字典keep_dic,通過在數(shù)字內(nèi)部移動火柴棒來改變該數(shù)字。函數(shù)依次枚舉了在數(shù)字a、b、c內(nèi)部移動火柴棒的情形,一旦找到滿足條件的等式,就將其作為答案插入到列表ans中。
函數(shù)fun_2()利用字典dec_dic和add_dic,通過在數(shù)字之間移動火柴棒同時改變2個數(shù)字。函數(shù)依次枚舉了從a拿一根火柴到b,從a拿一根火柴到c,從b拿一根火柴到a等6種情形。一旦找到滿足條件的等式,就將其作為答案插入到列表ans中。
函數(shù)fun_3()利用字典dec_dic或add_dic,通過在數(shù)字和運算符之間移動火柴棒同時改變1個數(shù)字和運算符。函數(shù)先判斷運算符是'+'還是'-',若為'+',則從運算符中拿一根火柴到數(shù)字,即把運算符改成'-',同時設(shè)置字典dic 的值為add_dic,表示數(shù)字要增加一根火柴棒;反之則設(shè)置成從數(shù)字中拿一根火柴到運算符。接下來依次枚舉在運算符與數(shù)字a、b、c間移動火柴的情形,一旦找到滿足條件的等式,就將其作為答案插入到列表ans中。
6.程序改進(jìn)
前面3個自定義函數(shù),功能都很明確,算法清晰明了,代碼實現(xiàn)也很直接,可讀性較強(qiáng)。但是它們都有一個缺陷,就是重復(fù)的代碼太多了,顯得有些拖沓和繁瑣。
我們經(jīng)常將重復(fù)出現(xiàn)的順序結(jié)構(gòu)程序轉(zhuǎn)換成循環(huán)結(jié)構(gòu),那么該如何精簡代碼呢?
以fun_1()函數(shù)為例,它用了3個結(jié)構(gòu)相同的一重循環(huán)語句,我們完全可以把它精簡成一個二重循環(huán)語句。
我們先把a(bǔ)、b、c三個變量存儲到列表role中,定義role = [a, b, c],這樣就可以用for循環(huán)遍歷它們了。因為a、b、c在算式中的位置不同,原fun_1()函數(shù)的3個for循環(huán)中的f字符串寫法也不一樣,但現(xiàn)在分別用role[0]、role[1]、role[2]來代替 a、b、c,就可以統(tǒng)一設(shè)置f字符串了。
根據(jù)這個思路,我編寫了如下代碼:

但實踐證明這段代碼是錯誤的。
問題出在哪里呢?
沒錯,問題就出在role[k] = i語句上。
當(dāng)k==0時,枚舉keep_dic[role[k]]就相當(dāng)于枚舉keep_dic[a],此時修改role[k] = i,相當(dāng)于修改數(shù)字a的值,但同時也修改了列表role的元素值,導(dǎo)致后面枚舉另兩個數(shù)字時,role[0]不再等于變量a的值,而是其移動火柴棒后的新數(shù)字。
那該怎么辦呢?既要使用列表遍歷三個數(shù)字,枚舉在它們內(nèi)部移動火柴棒后的可能值,又不能修改列表元素。有兩全其美的方法嗎?
當(dāng)然有!辦法就是在內(nèi)層循環(huán)中復(fù)制一份列表role的拷貝,然后在枚舉過程中修改這份拷貝,而不是role本身。參考代碼如下:

再來看fun_2()函數(shù),它用6個結(jié)構(gòu)相同的二重循環(huán),依次枚舉了在數(shù)字間移動火柴棒的6種情形。因為數(shù)字a、b、c在算式中的位置不同,原fun_2()函數(shù)的6個for循環(huán)中的f字符串寫法也不一樣,所以每一個二重循環(huán)的語句都有微妙的差別,合并代碼的難度很大。
但它們的結(jié)構(gòu)如此一致,肯定能夠用一個循環(huán)語句來代替重復(fù)的順序結(jié)構(gòu)!
必須要有堅定的信念,才能找到堅持的動力!
在精簡fun_1()函數(shù)的實踐過程中,我們采用了2個重要的方法:創(chuàng)建列表role存儲變量a、b、c,和在循環(huán)體內(nèi)創(chuàng)建列表role的拷貝op。
我們可以把舊有的經(jīng)驗遷移到解決新問題中去。
但這里有一個新的問題:從一個數(shù)字中拿走的火柴棒可能放到另外任意一個數(shù)字中,假設(shè)被拿走火柴棒的數(shù)字在列表role中的下標(biāo)為i,則另外兩個數(shù)字的下標(biāo)分別是多少呢?
若i=0,則另外兩個可以用i+1和i+2來表示;若i=1,則另外兩個可以用i-1和i+1來表示;若i=2,則另外兩個可以用i-2和i-1來表示。也就是沒有統(tǒng)一的表達(dá)式,不方便用變量i來表示另外兩個數(shù)字的下標(biāo)。
真的沒有辦法嗎?
辦法肯定是有的!只是還沒有找到。
必須要有堅定的信念,才能找到堅持的動力!
a,b,c變量的地位是相等的,不要把它們看作站成一排,而是把它們看作圍成一圈——這不就是約瑟夫環(huán)嗎!循環(huán)報數(shù)?用模運算?。?/span>
思路出來了!
我們可以用(i+1)%3和(i+2)%3分別表示另外兩個元素的下標(biāo),則當(dāng)i=0時,另外兩個下標(biāo)分別為1和2;當(dāng)i=1時,另外兩個下標(biāo)分別為2和0;當(dāng)i=2時,另外兩個下標(biāo)分別為0和1。
簡直是完美!
這樣我們就可以構(gòu)造一個for循環(huán),用變量j來遍歷這兩個下標(biāo),實現(xiàn)枚舉從數(shù)字role[i]中拿走一根火柴棒,增加到數(shù)字role[j]中的情形。加上遍歷列表role的最外層循環(huán),總共4重循環(huán)。參考代碼如下:

好了,解說就進(jìn)行到這里,剩下fun_3()函數(shù)的精簡任務(wù)就作為課后作業(yè)交給大家完成吧。
1. ?參考fun_1()函數(shù)的改進(jìn)方法,將fun_2()函數(shù)精簡為二重循環(huán)結(jié)構(gòu)。
2. ?有一種特殊的等式,可以移動其中一根火柴棒,使其變成另一個正確的等式。如下圖所示:

等式3+6=9,通過從數(shù)字6移動一根火柴棒到數(shù)字9中,形成新的等式3+5=8;
等式0+4=4,通過從加號中移動一根火柴棒到數(shù)字0中,形成新的等式8-4=4。
你還可以找出類似的等式嗎?如何用程序?qū)崿F(xiàn)呢?
需要本文PPT、源代碼和課后練習(xí)答案的,可以加入“Python算法之旅”知識星球參與討論和下載文件,“Python算法之旅”知識星球匯集了數(shù)量眾多的同好,更多有趣的話題在這里討論,更多有用的資料在這里分享。
我們專注Python算法,感興趣就一起來!
相關(guān)優(yōu)秀文章:
