出處:http://www.cnblogs.com/bigmonkey
本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途!
掃描二維碼關注公衆號「我是8位的」
小說《溥儀藏寶錄》講述了一個曲折離奇的故事。在故事中,溥儀試圖利用藏有大清皇家寶藏祕密的寶盒——「覲天寶匣」復辟清朝。這個寶匣是他從宮中帶走的惟一寶物,裏面藏着富可敵國的鉅額寶藏,足以發動第三次世界大戰。因爲種種緣由,溥儀將寶匣藏於太極皇陵。抗戰期間,愛國人士崔二侉子帶領衆人深刻太極皇陵,盜走了覲天寶匣。此後的幾年裏,參與盜寶的人陸續神祕死亡,崔二侉子將寶匣交給偵探出身的蕭劍南。其後六十多年的時間裏,蕭劍南用了一輩子時間試圖尋找到事情的真相,直到臨終,纔將這件事告知本身的孫子蕭偉。蕭偉與好友高陽、趙穎試圖打開覲天寶匣……node
寶匣共有三層,每層都有一鎖,第一層是「子午鴛鴦芯」,第二層是「對頂梅花芯」,而第三層是「天地乾坤芯」,由高麗制鎖名匠設計,沒有鑰匙,若是不是受過專門訓練,根本沒法開啓。任何外力企圖強行打開,都會觸發機關,啓動刀具裝置,將其中所藏之物絞得粉碎。算法
覲天寶匣的正上方刻有高麗名匠李舜臣在大韓海峽擊敗進犯日軍的場景,被切分紅九九八十一個小塊,組成了拼圖遊戲中最複雜的「九九拼圖」。 九九拼圖是覲天寶匣的護盾,只有將拼圖復原才能看到「子午鴛鴦芯」。在這裏,咱們感興趣的不是覲天寶匣的三重鎖,而是拼圖護盾,看看如何藉助計算機的幫助來破解護盾。app
第一步仍然是構建數據模型,創建從實際問題到軟件問題的映射。在書中,高陽想到了一個聰明的作法——把總體圖案用相機拍照,再把照片用PS切分紅小塊並一一編號,只要把編號移動至順序排列,問題就解決了 。dom
咱們使用高陽的作法,一個用一個n×n的二維列表存儲n×n的拼圖,列表中的每一個元素都是拼圖的一個小塊。對於一個被複原的三三拼圖來講,二維列表的數據:post
拼圖遊戲須要有一個「圖眼」,不然碎片沒法移動,咱們選擇右下角的碎片做爲圖眼:學習
可移動的碎片共有8個,編寫移動這8個碎片的代碼並不容易,須要加入大量的判斷。不妨換一種思路,在遊戲中只有圖眼能夠移動,這樣只須要將圖眼和目標位置的數據互相交換就能夠完成移動操做:spa
依然使用差向量表示上、右、下、左四個方向:(0, 1), (-1, 0), (0, -1), (1, 0),當圖眼向某個方向移動時,目標位置只須要用圖眼的當前位置加上該方向的差向量便可。咱們使用一個名爲JigsawPuzzle的類完成拼圖遊戲,它的基本數據模型以下:設計
1 class JigsawPuzzle: 2 def __init__(self, n=3): 3 self.n = n 4 # 成功狀態,列表元素按照從左到右,從上到下的順序依次排列 5 self.succ_img = [] 6 for i in range(n): 7 self.succ_img.append(list(range(n * i, n * i + n))) 8 # #用空白符號做爲圖眼的值 9 self.eye_val = ' ' 10 # 將右下角的碎片做爲圖眼 11 self.succ_img[n - 1][n - 1] = self.eye_val 12 # 「圖眼」移動的方向, 上、右、下、左 13 self.v_move = [(0, 1), (-1, 0), (0, -1), (1, 0)] 14 # 被打亂順序的拼圖 15 self.confuse_img = self._confuse() 16 # 已經被訪問過的拼圖狀態 17 self.visited_list = [] 18 # 拼圖步驟 19 self.answer = None 20 21 def _confuse(self): 22 ''' 將拼圖打亂順序 ''' 23 # 將圖眼隨機移動 n * n * 10 次 24 tar_img = copy.deepcopy(self.succ_img) 25 from_x, from_y = self.n - 1, self.n - 1 26 for i in range(self.n ** 2 * 10): 27 # 選擇一個隨機方向 28 v_x, v_y = random.choice(self.v_move) 29 to_x, to_y = from_x + v_x, from_y + v_y 30 if self.enable(to_x, to_y): 31 # 向選擇的隨機方向移動 32 self.move(tar_img, from_x, from_y, to_x, to_y) 33 from_x, from_y = to_x, to_y 34 35 return tar_img 36 37 def is_succ(self, curr_img): 38 ''' 39 是否完成拼圖 40 :param curr_img: 當前拼圖 41 :return: 42 ''' 43 for i, row in enumerate(curr_img): 44 for j, n in enumerate(row): 45 print(i, j, n, self.succ_img[i][j]) 46 if n != self.succ_img[i][j]: 47 return False 48 return True 49 50 def enable(self, to_x, to_y): 51 ''' 52 圖眼是否可以移動到to位置 53 :param to_x: 圖眼的行索引 54 :param to_y: 圖眼的列索引 55 :return: 56 ''' 57 return 0 <= to_x < self.n and 0 <= to_y < self.n 58 59 def move(self, curr_img, from_x, from_y, to_x, to_y): 60 ''' 61 將圖眼從from移動到to 62 ''' 63 curr_img[from_x][from_y], curr_img[to_x][to_y] = curr_img[to_x][to_y], curr_img[from_x][from_y]
enable()方法用於邊界校驗;move()用於移動圖眼,它作的僅僅是將列表中的兩個元素互換位置。_confuse()做爲私有方法,用於打亂拼圖的順序。像洗牌方法同樣隨機放置列表中的元素並不能保證拼圖必定可以還原,所以穩妥的方法是使用隨機移動若干次圖眼。code
第一個想到的策略仍然是盲目策略,窮舉全部的移動,直到拼圖還原爲止。這裏咱們選擇廣度優先搜索做爲搜索策略。blog
廣度優先搜索是另外一種盲目搜索算法,若是咱們把全部要搜索的狀態組成一棵樹,那麼廣度優先搜索就是按照層序搜索全部節點,直到搜完整棵樹爲止:
在拼圖中,圖眼每次至多能夠向四個方向移動,這四個方向構成了搜索的「一層」,每一層的狀態又能夠繼續展開:
注意到第三層的第四個狀態又回到了原點,繼續遍歷這個狀態是沒有意義的,在編寫代碼時可使用visited_list存儲全部被訪問過的拼圖狀態,若是碰到某一個狀態被訪問過,則直接略過:
1 def has(self, curr_img): 2 ''' 3 curr_img是否已經被訪問過 4 :param curr_img: 5 :return: 6 ''' 7 for s in self.visited_list: 8 if s == curr_img: 9 return True 10 return False
廣度優先搜索一般使用隊列的結構,樣本代碼以下:
1 from queue import Queue 2 def bfs(node): 3 ''' 圖的廣度優先搜索''' 4 if node is None: 5 return 6 queue = Queue() 7 nodeSet = set() 8 queue.put(node) 9 nodeSet.add(node) 10 while not queue.empty(): 11 cur = queue.get() # 彈出元素 12 for next in cur.nexts: # 遍歷元素的相鄰節點 13 if next not in nodeSet: # 若相鄰節點沒有入過隊列,加入隊列 14 nodeSet.add(next) 15 queue.put(next)
樣本代碼僅僅是遍歷了全部節點,而拼圖遊戲要作到的除了回答「通過多少遍歷才能能復原拼圖」以外,還要尋找復原的步驟,因此咱們須要構造一個結構將復原步驟存儲起來:
1 class Node: 2 ''' 拼圖狀態鏈表, 每個鏈表元素指向上一個拼圖狀態 ''' 3 def __init__(self, img, parent_node): 4 self.img = img 5 self.parent = parent_node
Node中存儲某一步拼圖的狀態,並用parent指向它的上一個狀態。如今可使用深度優先搜索的模板編寫代碼
1 def bfs(self): 2 ''' 廣度優先搜索 ''' 3 queue = Queue() 4 queue.put(Node(self.confuse_img, None)) 5 6 while not queue.empty(): 7 curr_node = queue.get() 8 curr_img = curr_node.img 9 self.visited_list.append(curr_img) 10 11 # 檢測拼圖是否正確 12 if self.is_succ(curr_img): 13 self.answer = curr_node 14 break 15 16 # curr_img中圖眼的位置 17 x, y = self.search_eye(curr_img) 18 # 向四個方向進行廣度優先搜索 19 for v_x, v_y in self.v_move: 20 to_x, to_y = x + v_x, y + v_y 21 if not self.enable(to_x, to_y): 22 continue 23 24 curr_copy = copy.deepcopy(curr_img) 25 self.move(curr_copy, x, y, to_x, to_y) 26 # 判斷curr_copy的狀態是否曾經搜索過 27 if not self.has(curr_copy): 28 next_node = Node(curr_copy, curr_node) 29 queue.put(next_node)
搜索過程將產生不少由不一樣的Node鏈表,只有最終指「復原」狀態的鏈表纔是有用的。
完整代碼:
1 from queue import Queue 2 import random 3 import copy 4 from os import system 5 import time 6 7 class Node: 8 ''' 拼圖狀態鏈表, 每個鏈表元素指向上一個拼圖狀態 ''' 9 def __init__(self, img, parent_node): 10 self.img = img 11 self.parent = parent_node 12 13 class JigsawPuzzle: 14 def __init__(self, n=3): 15 self.n = n 16 # 成功狀態,列表元素按照從左到右,從上到下的順序依次排列 17 self.succ_img = [] 18 for i in range(n): 19 self.succ_img.append(list(range(n * i, n * i + n))) 20 # #用空白符號做爲圖眼的值 21 self.eye_val = ' ' 22 # 將右下角的碎片做爲圖眼 23 self.succ_img[n - 1][n - 1] = self.eye_val 24 # 「圖眼」移動的方向, 上、右、下、左 25 self.v_move = [(0, 1), (-1, 0), (0, -1), (1, 0)] 26 # 被打亂順序的拼圖 27 self.confuse_img = self._confuse() 28 # 已經被訪問過的拼圖狀態 29 self.visited_list = [] 30 # 拼圖步驟 31 self.answer = None 32 33 def _confuse(self): 34 ''' 將拼圖打亂順序 ''' 35 # 將圖眼隨機移動 n * n * 10 次 36 tar_img = copy.deepcopy(self.succ_img) 37 from_x, from_y = self.n - 1, self.n - 1 38 for i in range(self.n ** 2 * 10): 39 # 選擇一個隨機方向 40 v_x, v_y = random.choice(self.v_move) 41 to_x, to_y = from_x + v_x, from_y + v_y 42 if self.enable(to_x, to_y): 43 # 向選擇的隨機方向移動 44 self.move(tar_img, from_x, from_y, to_x, to_y) 45 from_x, from_y = to_x, to_y 46 47 return tar_img 48 49 def is_succ(self, curr_img): 50 ''' 51 是否完成拼圖 52 :param curr_img: 當前拼圖 53 :return: 54 ''' 55 for i, row in enumerate(curr_img): 56 for j, n in enumerate(row): 57 print(i, j, n, self.succ_img[i][j]) 58 if n != self.succ_img[i][j]: 59 return False 60 return True 61 62 def enable(self, to_x, to_y): 63 ''' 64 圖眼是否可以移動到to位置 65 :param to_x: 圖眼的行索引 66 :param to_y: 圖眼的列索引 67 :return: 68 ''' 69 return 0 <= to_x < self.n and 0 <= to_y < self.n 70 71 def move(self, curr_img, from_x, from_y, to_x, to_y): 72 ''' 73 將圖眼從from移動到to 74 ''' 75 curr_img[from_x][from_y], curr_img[to_x][to_y] = curr_img[to_x][to_y], curr_img[from_x][from_y] 76 77 def has(self, curr_img): 78 ''' 79 curr_img是否已經被訪問過 80 :param curr_img: 81 :return: 82 ''' 83 for s in self.visited_list: 84 if s == curr_img: 85 return True 86 return False 87 def search_eye(self, img): 88 ''' 89 找到img中圖眼的位置 90 :param img: 91 :return: (x,y) 92 ''' 93 # 「圖眼」的值是eye_val,打亂順序後須要尋找到圖眼的位置 94 for x in range(self.n): 95 for y in range(self.n): 96 if self.eye_val == img[x][y]: 97 return x, y 98 99 def bfs(self): 100 ''' 廣度優先搜索 ''' 101 queue = Queue() 102 queue.put(Node(self.confuse_img, None)) 103 104 while not queue.empty(): 105 curr_node = queue.get() 106 curr_img = curr_node.img 107 self.visited_list.append(curr_img) 108 109 # 檢測拼圖是否正確 110 if self.is_succ(curr_img): 111 self.answer = curr_node 112 break 113 114 # curr_img中圖眼的位置 115 x, y = self.search_eye(curr_img) 116 # 向四個方向進行廣度優先搜索 117 for v_x, v_y in self.v_move: 118 to_x, to_y = x + v_x, y + v_y 119 if not self.enable(to_x, to_y): 120 continue 121 122 curr_copy = copy.deepcopy(curr_img) 123 self.move(curr_copy, x, y, to_x, to_y) 124 # 判斷curr_copy的狀態是否曾經搜索過 125 if not self.has(curr_copy): 126 next_node = Node(curr_copy, curr_node) 127 queue.put(next_node) 128 129 def start(self): 130 self.bfs() 131 132 def display(answer): 133 ''' 在控制檯打印動態效果(僅對三三拼圖有效) ''' 134 stack = [] 135 node = answer 136 while node is not None: 137 stack.append(node.img) 138 node = node.parent 139 while stack != []: 140 system('cls') 141 status_list = [i for item in stack.pop() for i in item] 142 print('''' 143 * * * * * 144 * %s %s %s * 145 * %s %s %s * 146 * %s %s %s * 147 * * * * *''' % tuple(status_list)) 148 time.sleep(1) 149 150 if __name__ == '__main__': 151 puzzle = JigsawPuzzle(n=3) 152 puzzle.start() 153 answer = puzzle.answer 154 while answer is not None: 155 print(answer.img) 156 answer = answer.parent 157 158 display(puzzle.answer)
若是拼圖的初始狀態是[[3, 0, 2], [1, 7, ' '], [6, 5, 4]],則程序打印的復原順序是:
[[0, 1, 2], [3, 4, 5], [6, 7, ' ']]
[[0, 1, 2], [3, 4, ' '], [6, 7, 5]]
[[0, 1, 2], [3, ' ', 4], [6, 7, 5]]
[[0, ' ', 2], [3, 1, 4], [6, 7, 5]]
[[' ', 0, 2], [3, 1, 4], [6, 7, 5]]
[[3, 0, 2], [' ', 1, 4], [6, 7, 5]]
[[3, 0, 2], [1, ' ', 4], [6, 7, 5]]
[[3, 0, 2], [1, 7, 4], [6, ' ', 5]]
[[3, 0, 2], [1, 7, 4], [6, 5, ' ']]
[[3, 0, 2], [1, 7, ' '], [6, 5, 4]]
打印結果自下而上構成了復原的每個步驟:
這種盲目搜索法對付3×3的小型拼圖尚可,當規模是9×9時就有些力不從心了。在dfs()中,每一移動一個碎片,都將產生4種新的移動,又是個指數爆炸的問題。可否在短期算出九九拼圖,全看運氣和人品,高陽的程序就運行了將近兩天時間。在下一章裏,咱們將繼續搜索的探討,使用更智能、更高效的搜索算法復原覲天寶匣的拼圖。
做者:我是8位的