一個“一筆畫”問題的求解器

埠默笙聲聲聲脈發表於2022-01-18

回老家跟侄子玩,因為不想慣著他玩手機的臭毛病,就跟他玩一些紙筆遊戲,比如這個:

從起點出發抵達終點,如何在不走重複格子的前提下,走過儘可能多的格子,並吃到儘可能多的金幣?其中,黑色格子是牆壁不能走。

感覺還挺有意思的,不妨就來寫一個“一筆畫”的求解器,找出所有最優的軌跡。

思路:

可以先輸出所有從起點到終點的軌跡,然後建立一個評分系統對所有軌跡進行評估:每走一個格子+1分,每吃一個金幣+1分,最後輸出評分最高的軌跡即可。

Step1:建模

一個如圖的迷宮可以很自然地用矩陣描述,對於每一個元素,值0表示空格子,值1表示金幣,值-1表示牆壁,值2表示起點,值3表示終點。

一個“一筆畫”問題的求解器
import numpy as np
ROW, COL = 6, 4
maze = np.zeros((ROW,COL))
START = (0, 3)
OUT = (5, 0)
maze[START] = 2
maze[OUT] = 3
maze[1, 1] = 1
maze[5, 3] = 1
maze[2, 1] = -1
maze[4, 2] = -1
View Code

Step2:尋跡演算法

一條軌跡可以拆分成多個“步”,在走每一步之前,所做的事情是一樣的:找到下一步能到達的合法位置,然後選擇其中一個。如果新位置是死路或者是終點,那麼結束或輸出該軌跡,並返回到上一步所在的位置,選擇其他步。這一過程可以很好地由遞迴函式表達:

def step(start, track, env):
    x, y = start
    next_pos = []
    for i, j in [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]:
        if 0 <= i <= ROW-1 and 0 <= j <= COL-1: # 邊界限制
            if (i, j) in track: # 重複的格子不能走
                continue
            if env[i,j] == -1: # 有牆壁的格子不能走
                continue
            next_pos.append((i, j))
    
    for i, j in next_pos: # 如果next_pos為空,不會進入迴圈,所以無需專門處理邊界情況
        track_new = track.copy()
        track_new.append((i, j))
        if (i, j) == OUT:
            global tracks
            tracks.append(track_new)
            return
        start_new = (i, j)
        step(start_new, track_new, env)

執行一下:

tracks = []
step(START, [START], maze) 
print(len(tracks))

輸出為192,說明從起點到終點的軌跡有192條。

Step3:評分器

每走一個格子+1分,每吃一個金幣+1分,輸出評分最高的軌跡。

def findOptimalTracks(tracks):
    # 統計所有track的分數
    # 分數:每走過一個格子+1,若是金幣格子則額外+1
    scores = []
    for track in tracks:
        score = 0
        if (1, 1) in track:
            score += 1
        if (5, 3) in track:
            score += 1
        score += len(track)
        scores.append(score)
    
    tracks_optimal = []
    maxScore = max(scores)
    for i, score in enumerate(scores):
        if score == maxScore:
            tracks_optimal.append(tracks[i])
    return tracks_optimal

執行一下:

tracks_optimal = findOptimalTracks(tracks)
print(len(tracks_optimal))

輸出為9,說明最優的軌跡有9條。

Step4:視覺化

其實事情到上一步就已經結束了,這裡就當看個樂子吧!用pygame把所有軌跡呈現出來:

class tracksRender():
    def __init__(self, tracks, env):
        pygame.init()
        SIZE = 75
        ROW, COL = env.shape
        self.window = pygame.display.set_mode((COL*SIZE, ROW*SIZE))
        pygame.display.set_caption('一筆畫')
        
        self.running = True
        self.clock = pygame.time.Clock() # 幀數控制
        self.time = time.time() # 時間控制
        self.INTERVAL = 0.1 # 動作間隔
        self.FPS = 30 # 幀數
        
        self.env = env
        self.tracks = tracks
        self.track = [] # 存放當前將顯示的軌跡
        self.track_render = [] # 存放當前顯示的部分軌跡
    
    
    def processInput(self):
        for event in pygame.event.get():
            # 按下右上角的退出鍵
            if event.type == pygame.QUIT:
                self.running = False
                break
    
    
    def update(self):
         if time.time() - self.time < self.INTERVAL: # 設定更新間隔
             return
         if self.tracks == []: # 如果已經顯示完所有軌跡 則函式返回
             return
         if self.track == []: # 如果當前軌跡已經完全顯示 則顯示下一條軌跡
             self.track = self.tracks.pop(0)
             self.track_render = []
         self.track_render.append(self.track.pop(0))
         self.time = time.time()
    
         
    def render(self):
        SIZE = 75
        ROW, COL = self.env.shape
        BGCOLOR = (226, 240, 217) # 淺草綠
        self.window.fill(BGCOLOR)
        DarkKhaki = (189, 183, 107) # 深卡其布
        Wall = (139, 126, 102) # 灰牆
        Orange = (255, 165, 0) # 橘子
        DodgerBlue = (30, 144, 255) # 道奇藍
        Gold = (255, 215, 0) # 黃金
        Font = pygame.font.SysFont('arial', 40)
        
        # 畫分隔線
        for i in range(ROW + 1):
            pygame.draw.line(self.window, DarkKhaki, (0, i*SIZE), (COL*SIZE, i*SIZE), 3)
        for i in range(COL + 1):
            pygame.draw.line(self.window, DarkKhaki, (i*SIZE, 0), (i*SIZE, ROW*SIZE), 3)
        
        # 畫特殊格子
        for i in range(ROW):
            for j in range(COL):
                if self.env[i, j] == -1: # 牆壁
                    pygame.draw.polygon(self.window, Wall, 
                                        ((j*SIZE, i*SIZE), ((j+1)*SIZE, i*SIZE), ((j+1)*SIZE, (i+1)*SIZE), (j*SIZE, (i+1)*SIZE))
                                        ,0)
                if self.env[i, j] == 2: # 起點
                    Start_text = Font.render('S', True, Orange)
                    self.window.blit(Start_text, ((j+0.3)*SIZE, (i+0.2)*SIZE))
                if self.env[i, j] == 3: # 終點
                    Start_text = Font.render('E', True, DodgerBlue)
                    self.window.blit(Start_text, ((j+0.3)*SIZE, (i+0.2)*SIZE))
                if self.env[i, j] == 1: # 金幣
                    pygame.draw.circle(self.window, Gold, ((j+0.5)*SIZE,(i+0.5)*SIZE), 20)
        
        # 畫軌跡線
        if len(self.track_render) > 1:
            for i, point in enumerate(self.track_render):
                if i == 0: continue
                x_, y_ = self.track_render[i-1]
                x, y = point
                pygame.draw.line(self.window, Orange, ((y_+0.5)*SIZE, (x_+0.5)*SIZE), ((y+0.5)*SIZE, (x+0.5)*SIZE), 5)                
        
        pygame.display.update() # 重新整理顯示
        
    def run(self):
        while self.running:
            self.processInput()
            self.update()
            self.render()
            self.clock.tick(self.FPS)

效果如下:

整體程式碼如下:

一個“一筆畫”問題的求解器
"""
走迷宮
玩家從起點出發,到達終點走出迷宮
迷宮可由矩陣描述,例如以下6×4的迷宮
[ 0  0  0  2
  0  1  0  0
  0 -1  0  0
  0  0  0  0
  0  0 -1  0
  3  0  0  1 ]
其中,2表示起點,3表示終點,-1表示牆壁(無法行動到該位置),1表示金幣
問,如何在不走重複格子的前提下,走出迷宮,並吃到最多的金幣,且走過最多的格子?請列印路徑
"""
import numpy as np
import pygame
import time

def step(start, track, env):
    x, y = start
    next_pos = []
    for i, j in [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]:
        if 0 <= i <= ROW-1 and 0 <= j <= COL-1: # 邊界限制
            if (i, j) in track: # 重複的格子不走
                continue
            if env[i,j] == -1: # 有牆壁的格子不能走
                continue
            next_pos.append((i, j))
            
    for i, j in next_pos:
        track_new = track.copy()
        track_new.append((i, j))
        if (i, j) == OUT:
            global tracks
            tracks.append(track_new)
            return
        start_new = (i, j)
        step(start_new, track_new, env)


def findOptimalTracks(tracks):
    # 統計所有track的分數
    # 分數:每走過一個格子+1,若是金幣格子則額外+1
    scores = []
    for track in tracks:
        score = 0
        if (1, 1) in track:
            score += 1
        if (5, 3) in track:
            score += 1
        score += len(track)
        scores.append(score)
    
    tracks_optimal = []
    maxScore = max(scores)
    for i, score in enumerate(scores):
        if score == maxScore:
            tracks_optimal.append(tracks[i])
    return tracks_optimal


class tracksRender():
    def __init__(self, tracks, env):
        pygame.init()
        SIZE = 75
        ROW, COL = env.shape
        self.window = pygame.display.set_mode((COL*SIZE, ROW*SIZE))
        pygame.display.set_caption('一筆畫')
        
        self.running = True
        self.clock = pygame.time.Clock() # 幀數控制
        self.time = time.time() # 時間控制
        self.INTERVAL = 0.1 # 動作間隔
        self.FPS = 30 # 幀數
        
        self.env = env
        self.tracks = tracks
        self.track = [] # 存放當前將顯示的軌跡
        self.track_render = [] # 存放當前顯示的部分軌跡
    
    
    def processInput(self):
        for event in pygame.event.get():
            # 按下右上角的退出鍵
            if event.type == pygame.QUIT:
                self.running = False
                break
    
    
    def update(self):
         if time.time() - self.time < self.INTERVAL: # 設定更新間隔
             return
         if self.tracks == []: # 如果已經顯示完所有軌跡 則函式返回
             return
         if self.track == []: # 如果當前軌跡已經完全顯示 則顯示下一條軌跡
             self.track = self.tracks.pop(0)
             self.track_render = []
         self.track_render.append(self.track.pop(0))
         self.time = time.time()
    
         
    def render(self):
        SIZE = 75
        ROW, COL = self.env.shape
        BGCOLOR = (226, 240, 217) # 淺草綠
        self.window.fill(BGCOLOR)
        DarkKhaki = (189, 183, 107) # 深卡其布
        Wall = (139, 126, 102) # 灰牆
        Orange = (255, 165, 0) # 橘子
        DodgerBlue = (30, 144, 255) # 道奇藍
        Gold = (255, 215, 0) # 黃金
        Font = pygame.font.SysFont('arial', 40)
        
        # 畫分隔線
        for i in range(ROW + 1):
            pygame.draw.line(self.window, DarkKhaki, (0, i*SIZE), (COL*SIZE, i*SIZE), 3)
        for i in range(COL + 1):
            pygame.draw.line(self.window, DarkKhaki, (i*SIZE, 0), (i*SIZE, ROW*SIZE), 3)
        
        # 畫特殊格子
        for i in range(ROW):
            for j in range(COL):
                if self.env[i, j] == -1: # 牆壁
                    pygame.draw.polygon(self.window, Wall, 
                                        ((j*SIZE, i*SIZE), ((j+1)*SIZE, i*SIZE), ((j+1)*SIZE, (i+1)*SIZE), (j*SIZE, (i+1)*SIZE))
                                        ,0)
                if self.env[i, j] == 2: # 起點
                    Start_text = Font.render('S', True, Orange)
                    self.window.blit(Start_text, ((j+0.3)*SIZE, (i+0.2)*SIZE))
                if self.env[i, j] == 3: # 終點
                    Start_text = Font.render('E', True, DodgerBlue)
                    self.window.blit(Start_text, ((j+0.3)*SIZE, (i+0.2)*SIZE))
                if self.env[i, j] == 1: # 金幣
                    pygame.draw.circle(self.window, Gold, ((j+0.5)*SIZE,(i+0.5)*SIZE), 20)
        
        # 畫軌跡線
        if len(self.track_render) > 1:
            for i, point in enumerate(self.track_render):
                if i == 0: continue
                x_, y_ = self.track_render[i-1]
                x, y = point
                pygame.draw.line(self.window, Orange, ((y_+0.5)*SIZE, (x_+0.5)*SIZE), ((y+0.5)*SIZE, (x+0.5)*SIZE), 5)                
        
        pygame.display.update() # 重新整理顯示
        
    def run(self):
        while self.running:
            self.processInput()
            self.update()
            self.render()
            self.clock.tick(self.FPS)


if __name__ == "__main__":
    ROW, COL = 6, 4
    maze = np.zeros((ROW,COL))
    START = (0, 3)
    OUT = (5, 0)
    maze[START] = 2
    maze[OUT] = 3
    maze[1, 1] = 1
    maze[5, 3] = 1
    maze[2, 1] = -1
    maze[4, 2] = -1
    print(' Maze:\n', maze)
    tracks = []
    step(START, [START], maze)    
    tracks_optimal = findOptimalTracks(tracks)
    tracksRender = tracksRender(tracks_optimal, maze)
    tracksRender.run()
    pygame.quit()  
View Code

 

相關文章