本文已做成視頻教程投稿b站(視頻版相對文本版有一些改進),點擊觀看視頻教程
本文主要通過三個實例來幫助大家理解遞歸(其展示動畫已上傳B站):
本文代碼已上傳到github:https://github.com/BigShuang/recursion-with-turtle
本文參考文獻:Problem Solving with Algorithms and Data Structures using Python
〇、遞歸算法三大原則
- 1、必須有一個基礎情形(base case)
- 2、遞歸算法必須改變狀態,來向1中的基礎情形靠攏
- 3、遞歸算法必須遞歸地調用自己本身。
初次接觸遞歸算法的人可能看不懂這三原則什么意思(很正常,我第一次就沒看懂orz)。
看不懂的話建議先看下面三個實戰(看看里面是如何具體應用這三大原則解決問題的),看完之后再回過頭來再看這三大原則,應該就很好理解了。
一、謝爾賓斯基三角形
謝爾賓斯基三角形是一種如下圖所示的分形,點擊觀看動畫
1,分析
其繪制過程如下
- 繪制一個三角形(一般為等邊三角形)
- 取三角形的三邊中點做頂點繪制一個小三角形,將上一級三角形分隔出三個小三角形。
- 對分隔出的三個小三角形重復步驟2。
繪制過程可以無限重復下去,但是對於我們編寫一個程序而言,必須要保證有窮性,所以需要在步驟三中對小三角形尺寸判斷,當小三角形小到一定程度時退出循環。
有窮性:一個算法必須總是(對任何合法的輸入值)在執行有窮步之后結束,且每一步都可在有窮時間內完成。
結合繪制過程,我們來使用遞歸三原則。分析一下遞歸部分應該要怎么寫
# 核心遞歸函數,主要實現繪制過程中的第二第三步 def draw_nextone(triangle,basesize): """ 根據指定三角形三邊中點繪制一個更小的三角形 然后再取指定三角形被分割出來的三個小三角形為參數,遞歸調用自身 :param triangle: 指定三角形三個頂點坐標,示例:((ax,ay),(bx,by),(cx,cy))。 :param basesize: 繪制三角形的最小尺寸,當三角形尺寸小於該參數時退出遞歸,相當於遞歸三原則第一條中的基礎情形(base case) """ # 根據triangle可以得到三角形三頂點(A,B,C)坐標,以及邊長l(三角形應該為等邊三角形) # 如果邊長l小於basesize,相當於到了遞歸算法的基礎情形,退出遞歸,即 # if l<basesize: # return # 根據三角形三頂點(A,B,C),算出三邊中點(D,E,F) # 以三邊中點(D,E,F)為頂點繪制出三角形(寫一個專門的函數,用於根據三角形三頂點坐標繪制出三角形) # 此時指定的三角形ABC應該被新繪制的小三角形DEF分割成三個小三角形 # 這三個小三角形應該分別為:ADF,DBE,FEC # 對這三個小三角形分別遞歸調用本函數draw_nextone,即 # draw_nextone(ADF,basesize) # draw_nextone(DBE,basesize) # draw_nextone(FEC,basesize)
2,代碼實現與回味
根據上一步的分析,繪制謝爾賓斯基三角形的最終代碼
import turtle import math t=turtle.Turtle() # 初始化turtle對象 t.hideturtle() # 隱藏turtle畫筆 t.speed(0) # 設置繪制速度。數值越小,繪制越快,0最快,10最慢 # ======= # 輔助用函數 # ======= def get_length(a,b): """返回a,b兩點之間的距離""" ax,ay=a bx,by=b return math.sqrt((ax-bx)**2+(ay-by)**2) def get_midpoint(a,b): """返回a,b兩點的中點坐標""" ax, ay = a bx, by = b return (ax + bx) / 2, (ay + by) / 2 def draw_triangle(a,b,c): """以a,b,c為頂點繪制三角形""" ax,ay=a bx,by=b cx,cy=c t.penup() t.goto(ax,ay) t.pendown() t.goto(bx,by) t.goto(cx,cy) t.goto(ax,ay) t.penup() # 核心遞歸函數,主要實現繪制過程中的第二第三步 def draw_nextone(triangle,basesize): """ 根據指定三角形三邊中點繪制一個更小的三角形 然后再取指定三角形被分割出來的三個小三角形為參數,遞歸調用自身 :param triangle: 指定三角形三個頂點坐標,示例:((ax,ay),(bx,by),(cx,cy))。 :param basesize: 繪制三角形的最小尺寸,當三角形尺寸小於該參數時退出遞歸,相當於遞歸三原則第一條中的基礎情形(base case) :return: None """ # 根據triangle可以得到三角形三頂點(A,B,C)坐標,以及邊長l(三角形應該為等邊三角形) a,b,c=triangle l=get_length(a,b) # 如果邊長l小於basesize,相當於到了遞歸算法的基礎情形,退出遞歸,即 if l<basesize: return # 根據三角形三頂點(A,B,C),算出三邊中點(D,E,F) d=get_midpoint(a,b) e=get_midpoint(b,c) f=get_midpoint(c,a) # 以三邊中點(D,E,F)為頂點繪制出三角形(寫一個專門的函數,用於根據三角形三頂點坐標繪制出三角形) draw_triangle(d,e,f) # 此時指定的三角形ABC應該被新繪制的小三角形DEF分割成三個小三角形 # 這三個小三角形應該分別為:ADF,DBE,FEC # 對這三個小三角形分別遞歸調用本函數draw_nextone,即 draw_nextone([a,d,f],basesize) draw_nextone([d,b,e],basesize) draw_nextone([f,e,c],basesize) # 繪制謝爾賓斯基三角形(原點為三角形中心,initsize為初始邊長,basesize為其中允許的最小三角形邊長) def draw_Sierpinski_triangle(initsize,basesize): # 根據初始邊長initsize算出三個頂點坐標 sign3=math.sqrt(3) # 根號3 ax,ay=0,initsize*sign3/3 bx,by=initsize/2,-initsize*sign3/6 cx,cy=-initsize/2,-initsize*sign3/6 a=(ax,ay) b=(bx,by) c=(cx,cy) draw_triangle(a,b,c) draw_nextone([a,b,c],basesize) turtle.done() # 定住窗口,不然繪制完窗口會閃退 if __name__ == '__main__': initsize=400 basesize=10 draw_Sierpinski_triangle(initsize,basesize)
拓展閱讀:http://interactivepython.org/runestone/static/pythonds/Recursion/pythondsSierpinskiTriangle.html
二、漢諾塔
漢諾塔:漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞着64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。
抽象成數學問題:
有三根相鄰的柱子,標號為A,B,C,A柱子上從下到上按金字塔狀疊放着n個大小不同(下面的大於上面)的圓盤,要把所有盤子一個一個移動到柱子C上,並且每次移動同一根柱子上都不能出現大盤子在小盤子上方,求移動過程。
n=7時如下圖:
1,思路分析
對於漢諾塔上的n個圓盤,我們從上到下(也是從小到大)分別用1,2,3、、、n去命名。
A為起點塔,C為目標塔,B為中轉塔(中轉塔的意義通過后文會明白)
- 當n=1時,如上圖2-1所示
1 只需把圓盤1從起點塔A移到目標塔C即可。
- 當n=2時,如上圖2-2所示
1 先將圓盤1從起點塔A移動到中轉塔B
2 再將圓盤2從起點塔A移動到目標塔C
3 最后將圓盤1從中轉塔B(中轉塔)移動到目標塔C
- 當n=3時
1 先將圓盤12看作一個整體從起點塔A移動到中轉塔B(具體移動方法可看n=2,上面已經記錄了n=2時的移動方法,只不過在本次移動過程中,B是本次移動的目標塔,C則是本次移動的中轉塔)
2 再將圓盤3從起點塔A移動到目標塔C
3 最后將圓盤12看作一個整體從中轉塔B移動到目標塔C(可參考本情況的步驟一)
那么順着這個邏輯往下走,我們不難猜想到
- 當n=k時(k>1)
1 先將圓盤1到k-1看作一個整體從起點塔A移動到中轉塔B
2 再將圓盤k從起點塔A移動到目標塔C
3 最后將圓盤1到k-1看作一個整體從中轉塔B移動到目標塔C
上面的這個過程可以算是分治算法(而這個算法往往通過遞歸的方式去實現)
2,分治法
分治法,簡單的來講,就是分而治之(英文:Divide and Conquer);其核心思想,就是把一個復雜的大問題,拆分成一些規模較小的子問題(與原問題相同或相似),並可以對這些子問題反復地(遞歸地)執行這個過程,直至問題規模減小到可以求解(這種情況對應遞歸算法三原則中的基礎情況)。
具體到漢諾塔,那么復雜的大問題就是對於n層的漢諾塔,我們該怎么去從起始塔A移動到終止塔C,這個問題難以直接解決,
那么我們首先把這個問題拆分成三個規模較小的問題:
- 1 先將圓盤1到n-1看作一個整體從起點塔A移動到中轉塔B(用塔C作為本步驟的中轉)
- 2 再將圓盤n從起點塔A移動到目標塔C
- 3 最后將圓盤1到n-1看作一個整體從中轉塔B移動到目標塔C(用塔A作為本步驟的中轉)
其中2可以直接解決,1和3仍然難以直接解決,那么我們可以將1繼續拆分成是那個規模更小的問題(3類似):
- 1-1 先將圓盤1到n-2看作一個整體從A移動到C(用塔B作為本步驟的中轉)
- 1-2 再將圓盤n-1從起點塔A移動到B
- 1-3 最后將圓盤1到n-2看作一個整體從C移動到A(用塔A作為本步驟的中轉)
其中1-2可以直接解決,對於1-1和1-3我們可以繼續拆分,直至只剩一個圓盤的情況,此時即可直接解決。
上過高中的小伙伴應該都學過數學歸納法,其實上面的這些也可以用數學歸納法去做一個嚴謹的證明。
3,數學歸納法
這一步是為了給上面的分析提供一個嚴謹的證明,對思路有一定啟發,但是不看問題也不大,不太感興趣的話可以直接跳過~
數學歸納法的基本步驟分兩步:
- 證明當n= 1時命題成立。
- 假設n=m時命題成立,那么可以推導出在n=m+1時命題也成立。(m代表任意自然數)
對於本問題,n層漢諾塔
- 當n=1時,只需把圓盤1從起點塔A移到目標塔C即可。
- 假設n=m時,圓盤1-m可以通過上文討論的方法從起點塔A移到目標塔C,那么圓盤1-m也可以從A移動到B,然后圓盤m+1可以移動到C,最后圓盤1-m可以從B移動到C。
所以上文討論的方法對於所有正整數n都是有效的。
4,代碼實現
代碼如下
# 移動指定層圓盤diskIndex,從fromPole出發,到達toPole def moveDisk(diskIndex,fromPole,toPole): """ :param diskIndex: 圓盤的索引(從上往下,第一層為1,第二層為2、、、第n層為n) :param fromPole: 出發的柱子(起點) :param toPole: 要到達的柱子(終點) :return: """ print_str='Move disk %s form %s to %s'%(diskIndex,fromPole,toPole) print(print_str) # 核心函數,入口 def moveTower(height,fromPole, withPole, toPole): """ :param height: 漢諾塔高度——層數 :param fromPole: 出發的柱子(起點) :param withPole: 進過的柱子(中轉點) :param toPole: 要到達的柱子(終點) :return: """ if height == 1: # 基礎情形:一層的漢諾塔 moveDisk(1,fromPole, toPole) return # 先將圓盤1到n - 1看作一個整體從起點塔移動到中轉塔(用目標塔作為本步驟的中轉) moveTower(height-1,fromPole,toPole,withPole) # 再將圓盤n從起點塔A移動到目標塔C moveDisk(height,fromPole,toPole) # 最后將圓盤1到n - 1看作一個整體從中轉塔移動到目標塔(用起點塔作為本步驟的中轉) moveTower(height-1,withPole,fromPole,toPole) if __name__ == '__main__': # 調用 # 三層漢諾塔,A為出發柱子,B為中轉柱子,C為目標柱子 moveTower(3,"A","B","C")
本代碼輸入如下(此時為三層漢諾塔)
Move disk 1 form A to C Move disk 2 form A to B Move disk 1 form C to B Move disk 3 form A to C Move disk 1 form B to A Move disk 2 form B to C Move disk 1 form A to C
5,可視化漢諾塔代碼實現(使用turtle)
代碼如下
import turtle # ============== # 常量設置 # ============== N=3 # 漢諾塔層數限制 BasePL=12 # plate的大小基數,修改這個能夠調整plate的大小 TowerP=5 # Tower的線寬 TowerW=110 # Tower的底座寬度 TowerH=200 # Tower的高度 TowerSpace=260 # Tower的之間的距離,從中心到中心 HORIZON=-100 # Tower的底座高度,用於定位 # 動畫速度,5是比較適中的速度 PMS=5 # 優化處理 Isjump=True POLES={ "1": [], "2": [], "3": [], } PLATES=[] # 存儲所有圓盤對象 # 塔的顏色 LineColor="black" # 多個盤子的顏色 FillColors=[ "#d25b6a", "#d2835b", "#e5e234", "#83d05d", "#2862d2", "#35b1c0", "#5835c0" ] # 建立窗體 SCR=turtle.Screen() # SCR.tracer() SCR.setup(800,600) #設置窗體大小 # 設置圓盤形狀 def set_plate(pi=0): _pi=pi+2 t = turtle.Turtle() t.hideturtle() t.speed(0) t.penup() t.begin_poly() t.left(90) t.forward(BasePL*_pi) t.circle(BasePL, 180) t.forward(BasePL * 2 * _pi) t.circle(BasePL, 180) t.forward(BasePL * _pi) t.end_poly() p = t.get_poly() pname='plate_%s'%pi SCR.register_shape(pname, p) # 設置塔柱形狀 def set_tower(): t = turtle.Turtle() t.hideturtle() t.speed(0) t.penup() t.begin_poly() t.left(90) t.forward(TowerW) t.circle(-TowerP, 180) t.forward(TowerW) t.forward(TowerW) t.circle(-TowerP, 180) t.forward(TowerW-TowerP/2) t.left(90) t.forward(TowerH) t.circle(-TowerP, 180) t.forward(TowerH) t.end_poly() p = t.get_poly() SCR.register_shape('tower', p) # 繪制塔柱 def draw_towers(): set_tower() for tx in [-TowerSpace,0,TowerSpace]: t3 = turtle.Turtle('tower') t3.penup() t3.goto(tx,HORIZON) # 繪制圓盤 def draw_plates(pn=4): plates=[] for i in range(pn): set_plate(i) _plate='plate_%s'%i _p=turtle.Turtle(_plate) _colorIdx = i % len(FillColors) _color=FillColors[_colorIdx] _p.color(_color,_color) _p.speed(PMS) plates.append(_p) # 反序,大的在前,小的在后 global PLATES PLATES = plates[:] # 繪制移動過程 def draw_move(diskIndex, fromPindex, toPindex): p=PLATES[diskIndex-1] index_loc={ "A":1, "B":2, "C":3 } toP=index_loc.get(toPindex,None) fromP=index_loc.get(fromPindex,None) p.penup() mx = (toP - 2) * TowerSpace my = HORIZON + len(POLES[str(toP)]) * BasePL * 2 if fromP!=None: POLES[str(fromP)].remove(p) if Isjump: px,py=p.pos() p.goto(px,TowerH+py) p.goto(mx,TowerH+py) p.goto(mx, my) POLES[str(toP)].append(p) # 將所有圓盤移動到起點 def movetoA(n,fromPindex): for i in range(n,0,-1): draw_move(i,None,fromPindex) # 移動指定層圓盤diskIndex,從fromPole出發,到達toPole def moveDisk(diskIndex,fromPole,toPole): """ :param diskIndex: 圓盤的索引(從上往下,第一層為1,第二層為2、、、第n層為n) :param fromPole: 出發的柱子(起點) :param toPole: 要到達的柱子(終點) :return: """ draw_move(diskIndex, fromPole, toPole) # 核心函數,入口 def moveTower(height,fromPole, withPole, toPole): """ :param height: 漢諾塔高度——層數 :param fromPole: 出發的柱子(起點) :param withPole: 進過的柱子(中轉點) :param toPole: 要到達的柱子(終點) :return: """ if height == 1: # 基礎情形:一層的漢諾塔 moveDisk(1,fromPole, toPole) return # 先將圓盤1到n - 1看作一個整體從起點塔移動到中轉塔(用目標塔作為本步驟的中轉) moveTower(height-1,fromPole,toPole,withPole) # 再將圓盤n從起點塔A移動到目標塔C moveDisk(height,fromPole,toPole) # 最后將圓盤1到n - 1看作一個整體從中轉塔移動到目標塔(用起點塔作為本步驟的中轉) moveTower(height-1,withPole,fromPole,toPole) if __name__ == '__main__': # 調用 # 三層漢諾塔,A為出發柱子,B為中轉柱子,C為目標柱子 n=3 SCR.tracer(0) draw_towers() draw_plates(n) movetoA(n,"A") SCR.tracer(1) SCR.delay(1) moveTower(n,"A","B","C") turtle.done()
三、迷宮探索
迷宮如下圖所示:
迷宮文本301.txt如下
迷宮文本需要在代碼所在文件夾里面,新建一個text文件夾,放在text文件夾里面
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 2 0 2 0 0 0 0 0 2 0 0 0 0 0 2 0 0 0 2 0 0 0 0 0 0 0 0 0 1 1 0 1 0 1 0 1 0 1 2 1 2 1 0 1 2 1 2 1 0 1 2 1 0 1 2 1 2 1 0 1 2 1 1 1 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 2 0 2 0 0 0 0 0 0 0 2 0 0 0 1 1 1 1 0 1 2 1 2 1 2 1 0 1 2 1 0 1 2 1 0 1 0 1 0 1 0 1 0 1 2 1 0 1 1 1 0 0 S 0 0 0 0 0 0 0 2 0 2 0 0 0 0 0 0 0 2 0 2 0 2 0 0 0 2 0 1 1 1 1 0 1 0 1 2 1 2 1 2 1 0 1 0 1 2 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 0 0 2 0 0 0 2 0 0 0 0 0 2 0 0 0 2 0 2 0 2 0 2 0 2 0 2 0 2 0 1 1 1 1 0 1 0 1 2 1 0 1 0 1 0 1 2 1 2 1 0 1 0 1 0 1 0 1 2 1 0 1 2 1 1 1 0 0 2 0 0 0 0 0 2 0 2 0 0 0 2 0 2 0 2 0 2 0 2 0 0 0 2 0 2 0 1 1 1 1 0 1 0 1 0 1 0 1 2 1 2 1 2 1 0 1 2 1 0 1 0 1 0 1 2 1 0 1 0 1 1 1 0 0 2 0 2 0 2 0 0 0 0 0 0 0 0 0 0 0 2 0 2 0 2 0 0 0 2 0 0 0 1 1 1 1 0 1 2 1 2 1 0 1 2 1 2 1 0 1 0 1 2 1 2 1 2 1 2 1 2 1 0 1 0 1 1 1 0 0 2 0 0 0 0 0 0 0 0 0 2 0 2 0 0 0 0 0 0 0 0 0 0 0 2 0 2 0 1 1 1 1 2 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 2 1 0 1 0 1 2 1 0 1 2 1 1 1 0 0 0 0 2 0 2 0 2 0 2 0 2 0 2 0 2 0 2 0 0 0 2 0 0 0 2 0 2 0 1 1 1 1 2 1 0 1 2 1 2 1 2 1 2 1 2 1 0 1 0 1 2 1 0 1 2 1 0 1 0 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2 0 2 0 2 0 0 0 0 0 2 0 2 0 0 0 1 1 1 1 0 1 2 1 2 1 0 1 2 1 2 1 0 1 2 1 0 1 0 1 0 1 2 1 0 1 2 1 0 1 1 1 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 2 0 2 0 0 E 2 0 2 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
其中1和2都是牆,0是路,S為起點,E為終點。
1,實現迷宮可視化(使用turtle)
編寫函數用於讀取迷宮,存到一個二維的列表里面。代碼如下
# 從txt文本中將迷宮提取出來 def get_maze_info(filename): with open(filename,'r') as f: fl = f.readlines() maze_list =[] for line in fl: line = line.strip() line_list = line.split(" ") maze_list.append(line_list) return maze_list
編寫具體繪制的方法
# 讀取迷宮文本信息 txt_path = "text/301.txt" mazeList = get_maze_info(txt_path) # 獲取迷宮的長寬,R-行數,C-列數 R, C = len(mazeList), len(mazeList[0]) # 設置用於繪制迷宮的單元格的尺寸 cellsize = 20 import turtle # 導入turtle庫 scr = turtle.Screen() # 建立屏幕對象 src scr.setup(width=C*cellsize,height=R*cellsize) # 設置屏幕尺寸大小(剛好能夠顯示所有迷宮的單元格) t = turtle.Turtle() # 建立畫筆對象 t t.speed(0) # 設置畫筆對象的速度,0是最快的,1-10依次變快 # 根據迷宮信息繪制迷宮 def draw_maze(mazeList): scr.tracer(0) # 具體我也沒搞明白,只知道能夠跳過漫長的繪制動畫 # rowIndex,行號,相當於y for rowIndex in range(len(mazeList)): row = mazeList[rowIndex] for columnIndex in range(len(row)): # columnIndex,列號,相當於x item = row[columnIndex] if item == "1" or item == "2": draw_cell(columnIndex,rowIndex) # 繪制具體的單元格 def draw_cell(ci,ri): """ 繪制一個牆體單元格, :param ci: 單元格所在列序號 :param ri: 單元格所在行序號 :return: """ # 計算出單元格左上角的坐標,計算演示圖在本段代碼下方 tx = ci*cellsize - C*cellsize/2 ty = R*cellsize/2 - ri*cellsize t.penup() # 提起畫筆 t.goto(tx,ty) # 根據計算出來的坐標移動到單元格左上角 t.pendown() # 放下畫筆 t.begin_fill() # 開啟填充,此時經過的形狀會被填充 # 繪制一個邊長為cellsize的正方形 for i in range(4): t.fd(cellsize) t.right(90) t.end_fill() # 關閉填充
計算單元格坐標演示圖
調用上面寫好的方法就可以繪制出迷宮了
draw_maze(mazeList) # 繪制玩的話會發現窗口會自動退出掉,加入下面一行代碼就可以維持住窗口 turtle.done()
此時繪制出來會如下圖
我們這樣全黑的迷宮樣式上並不美觀,可以設置不同的灰度值讓黑色有變化,從而更加耐看
做法也很簡單,只需要簡單修改下draw_cell 方法就好(在開頭和中間部分各加上兩行代碼,如下)
import random # 導入隨機數模塊 scr.colormode(255) # 設置顏色模式 def draw_cell(ci,ri): """ 繪制一個牆體單元格, :param ci: 單元格所在列序號 :param ri: 單元格所在行序號 :return: """ # 計算出單元格左上角的坐標,計算演示圖在本段代碼下方 tx = ci*cellsize - C*cellsize/2 ty = R*cellsize/2 - ri*cellsize t.penup() # 提起畫筆 t.goto(tx,ty) # 根據計算出來的坐標移動到單元格左上角 # 為了美化樣式,我們需要把黑色弄的有變化些,即有不同的灰度 v = random.randint(100,150) t.color(v,v,v) t.pendown() # 放下畫筆 t.begin_fill() # 開啟填充,此時經過的形狀會被填充 # 繪制一個邊長為cellsize的正方形 for i in range(4): t.fd(cellsize) t.right(90) t.end_fill() # 關閉填充
此時繪制出來的迷宮如下圖
然后迷宮需要再把起點和終點繪制出來。
寫一個專門的方法draw_dot去繪制點(可以設置顏色)
# 新建一個畫筆對象用於繪制點 dot_t = turtle.Turtle() # 設置打點的尺寸 dot_size = 15 def draw_dot(ci,ri,color = "black"): """ 在制定單元格繪制圓點, :param ci: 單元格所在列序號 :param ri: 單元格所在行序號 :param color: 圓點的顏色 :return: """ # 計算出單元格左上角的坐標,計算演示圖在本段代碼下方 tx = ci * cellsize - C * cellsize / 2 ty = R * cellsize / 2 - ri * cellsize # 進一步計算出所在單元格中心的坐標 cx = tx + cellsize / 2 cy = ty - cellsize / 2 dot_t.penup() dot_t.goto(cx,cy) dot_t.dot(dot_size,color)
然后再在函數draw_maze里面加入對起點和終點的判斷與繪制(其實就是在最后面加上四行代碼),改動后的方法代碼如下
# 根據迷宮信息繪制迷宮 def draw_maze(mazeList): scr.tracer(0) # 具體我也沒搞明白,只知道能夠跳過漫長的繪制動畫 # rowIndex,行號,相當於y for rowIndex in range(len(mazeList)): row = mazeList[rowIndex] for columnIndex in range(len(row)): # columnIndex,列號,相當於x item = row[columnIndex] if item == "1" or item == "2": draw_cell(columnIndex, rowIndex) # 繪制具體的單元格 elif item == "S": # 設置起點顏色為藍色 draw_dot(columnIndex, rowIndex,"blue") elif item == "E": # 設置終點顏色為綠色 draw_dot(columnIndex, rowIndex, "green")
此時繪制出來的迷宮如下圖
2,尋路思路分析
使用上面遞歸三原則原理來分析以下
其探索過程為:
從起點出發,分別按順序往上下左右四個方向去探索(即移動到上下左右的相鄰單元格),
在這一過程中遞歸地講對探索后的相鄰單元格進行進一步四周的探索(即將該相鄰單元格當做新的起點去執行上一步驟,直至探索完成或失敗,才開始下一個方向的探索)
探索的具體過程可以分下面幾種情況:
- 找到終點,探索完成,然后告訴上一步這一步探索成功
- 找到牆或者探索過的點(或者超出迷宮的點),探索失敗,然后還是告訴上一步這一步探索是失敗的
- 向某個方向的探索得出的結論是成功的(源於1),那么探索完成,不在探索,並且告訴上一步探索這一方向是能夠探索成功的
- 向某個方向的探索得出的結論是失敗的(源於2),那么換一個方向進行探索
- 向所有方向探索都失敗了,那么探索失敗,並告訴上一步這一方向探索是失敗的
代碼如下
# 新建一個畫筆對象用於繪制探索過程(也就是路徑) line_t = turtle.Turtle() line_t.pensize(5) line_t.speed(0) def start_search(mazeList): # 獲取起點所在行和列的序號 start_c, start_r = 0, 0 # rowIndex,行號,相當於y for rowIndex in range(len(mazeList)): row = mazeList[rowIndex] for columnIndex in range(len(row)): # columnIndex,列號,相當於x item = row[columnIndex] if item == "S": start_c, start_r = columnIndex, rowIndex line_t.penup() draw_path(start_c, start_r) line_t.pendown() # 進入遞歸搜索 searchNext(mazeList, start_c, start_r) # 核心遞歸探索方法,從該點出發,遞歸地去探索四個方向 def searchNext(mazeList, ci, ri): # 1,找到終點,探索完成,然后告訴上一步這一步探索成功 if mazeList[ri][ci] == "E": draw_path(ci, ri) return True # 2,找到牆或者探索過的點(或者超出迷宮的點),探索失敗,然后還是告訴上一步這一步探索是失敗的 if not (0 <= ci < len(mazeList[0]) and 0 <= ri < len(mazeList)): return False if mazeList[ri][ci] in ["1", "2","TRIED"]: return False # 探索后標記該點為已探索過 mazeList[ri][ci] = "TRIED" draw_path(ci, ri) # 上下左右四個探索的方向 direction = [ [1, 0], [-1, 0], [0, 1], [0, -1], ] for d in direction: dc, dr =d found = searchNext(mazeList, ci + dc, ri + dr) if found: # 3,向某個方向的探索得出的結論是成功的(源於1),那么探索完成,不在探索,並且告訴上一步探索這一方向是能夠探索成功的 draw_path(ci, ri, "green") return True else: # 4,向某個方向的探索得出的結論是失敗的(源於2),那么換一個方向進行探索 draw_path(ci, ri, "red") # 5,向所有方向探索都失敗了,那么探索失敗,並告訴上一步這一方向探索是失敗的 return False def draw_path(ci,ri,color = "blue"): # 計算出單元格左上角的坐標,計算演示圖在本段代碼下方 tx = ci * cellsize - C * cellsize / 2 ty = R * cellsize / 2 - ri * cellsize # 進一步計算出所在單元格中心的坐標 cx = tx + cellsize / 2 cy = ty - cellsize / 2 line_t.color(color) line_t.goto(cx, cy)
最后調用該方法,效果就和我在b站的投稿差不多了
draw_maze(mazeList) scr.tracer(1) start_search(mazeList) # 繪制玩的話會發現窗口會自動退出掉,加入下面一行代碼就可以維持住窗口 turtle.done()
尋路完如下圖所示
3,最終代碼
梳理代碼后如下
import turtle import random # ======================== # 常量 # ======================== # 設置用於繪制迷宮的單元格的尺寸 CELL_SIZE = 20 # 設置打點(起點和終點)的尺寸 DOT_SIZE = 15 # 設置探索過程(也就是路徑)的尺寸 LINE_SIZE = 5 TXT_PATH = "text/301.txt" # ======================== # 初始化一些turtle畫筆對象 # ======================== scr = turtle.Screen() # 建立屏幕對象 src scr.colormode(255) # 設置顏色模式為rgb數值模式 wall_t = turtle.Turtle() # 建立畫筆對象 wall_t 用於繪制牆體 dot_t = turtle.Turtle() # 建立畫筆對象 dot_t 用於繪制點 line_t = turtle.Turtle() # 建立畫筆對象 line_t 用於繪制探索過程(也就是路徑) line_t.pensize(LINE_SIZE) # 從txt文本中將迷宮提取出來 def get_maze_info(filename): with open(filename, 'r') as f: fl = f.readlines() maze_list = [] for line in fl: line = line.strip() line_list = line.split(" ") maze_list.append(line_list) return maze_list mazeList = get_maze_info(TXT_PATH) # 獲取迷宮的長寬,R-行數,C-列數 R, C = len(mazeList), len(mazeList[0]) scr.setup(width=C * CELL_SIZE, height=R * CELL_SIZE) # 設置屏幕尺寸大小(剛好能夠顯示所有迷宮的單元格) def draw_cell(ci, ri): """ 繪制一個牆體單元格, :param ci: 單元格所在列序號 :param ri: 單元格所在行序號 :return: """ # 計算出單元格左上角的坐標,計算演示圖在本段代碼下方 tx = ci * CELL_SIZE - C * CELL_SIZE / 2 ty = R * CELL_SIZE / 2 - ri * CELL_SIZE wall_t.penup() # 提起畫筆 wall_t.goto(tx, ty) # 根據計算出來的坐標移動到單元格左上角 # 為了美化樣式,我們需要把黑色弄的有變化些,即有不同的灰度 v = random.randint(100, 150) wall_t.color(v, v, v) wall_t.pendown() # 放下畫筆 wall_t.begin_fill() # 開啟填充,此時經過的形狀會被填充 # 繪制一個邊長為CELL_SIZE的正方形 for i in range(4): wall_t.fd(CELL_SIZE) wall_t.right(90) wall_t.end_fill() # 關閉填充 def draw_dot(ci, ri, color="black"): """ 在制定單元格繪制圓點, :param ci: 單元格所在列序號 :param ri: 單元格所在行序號 :param color: 圓點的顏色 :return: """ # 計算出單元格左上角的坐標,計算演示圖在本段代碼下方 tx = ci * CELL_SIZE - C * CELL_SIZE / 2 ty = R * CELL_SIZE / 2 - ri * CELL_SIZE # 進一步計算出所在單元格中心的坐標 cx = tx + CELL_SIZE / 2 cy = ty - CELL_SIZE / 2 dot_t.penup() dot_t.goto(cx, cy) dot_t.dot(DOT_SIZE, color) # 根據迷宮信息繪制迷宮 def draw_maze(mazeList): scr.tracer(0) # 具體我也沒搞明白,只知道能夠跳過漫長的繪制動畫 # rowIndex,行號,相當於y for rowIndex in range(len(mazeList)): row = mazeList[rowIndex] for columnIndex in range(len(row)): # columnIndex,列號,相當於x item = row[columnIndex] if item == "1" or item == "2": draw_cell(columnIndex, rowIndex) # 繪制具體的單元格 elif item == "S": # 設置起點顏色為藍色 draw_dot(columnIndex, rowIndex, "blue") elif item == "E": # 設置終點顏色為綠色 draw_dot(columnIndex, rowIndex, "green") # 繪制路徑,以畫筆當前所在位置為起點,以單元格(ci, ri)中心作為終點,繪制路徑。 def draw_path(ci, ri, color="blue"): # 計算出單元格左上角的坐標,計算演示圖在本段代碼下方 tx = ci * CELL_SIZE - C * CELL_SIZE / 2 ty = R * CELL_SIZE / 2 - ri * CELL_SIZE # 進一步計算出所在單元格中心的坐標 cx = tx + CELL_SIZE / 2 cy = ty - CELL_SIZE / 2 line_t.color(color) line_t.goto(cx, cy) # 核心遞歸探索方法,從該點出發,遞歸地去探索四個方向 def searchNext(mazeList, ci, ri): # 1,找到終點,探索完成,然后告訴上一步這一步探索成功 if mazeList[ri][ci] == "E": draw_path(ci, ri) return True # 2,找到牆或者探索過的點(或者超出迷宮的點),探索失敗,然后還是告訴上一步這一步探索是失敗的 if not (0 <= ci < len(mazeList[0]) and 0 <= ri < len(mazeList)): return False if mazeList[ri][ci] in ["1", "2", "TRIED"]: return False # 探索后標記該點為已探索過 mazeList[ri][ci] = "TRIED" draw_path(ci, ri) # 上下左右四個探索的方向 direction = [ [1, 0], [-1, 0], [0, 1], [0, -1], ] for d in direction: dc, dr = d found = searchNext(mazeList, ci + dc, ri + dr) if found: # 3,向某個方向的探索得出的結論是成功的(源於1),那么探索完成,不在探索,並且告訴上一步探索這一方向是能夠探索成功的 draw_path(ci, ri, "green") return True else: # 4,向某個方向的探索得出的結論是失敗的(源於2),那么換一個方向進行探索 draw_path(ci, ri, "red") # 5,向所有方向探索都失敗了,那么探索失敗,並告訴上一步這一方向探索是失敗的 return False # 開始迷宮探索 def start_search(mazeList): # 獲取起點所在行和列的序號 start_c, start_r = 0, 0 # rowIndex,行號,相當於y for rowIndex in range(len(mazeList)): row = mazeList[rowIndex] for columnIndex in range(len(row)): # columnIndex,列號,相當於x item = row[columnIndex] if item == "S": start_c, start_r = columnIndex, rowIndex line_t.penup() draw_path(start_c, start_r) line_t.pendown() # 進入遞歸搜索 searchNext(mazeList, start_c, start_r) draw_maze(mazeList) scr.tracer(1) start_search(mazeList) # 繪制玩的話會發現窗口會自動退出掉,加入下面一行代碼就可以維持住窗口 turtle.done()