搜索的策略(3)——覲天寶匣上的拼圖

  小說《溥儀藏寶錄》講述了一個曲折離奇的故事。在故事中,溥儀試圖利用藏有大清皇家寶藏祕密的寶盒——「覲天寶匣」復辟清朝。這個寶匣是他從宮中帶走的惟一寶物,裏面藏着富可敵國的鉅額寶藏,足以發動第三次世界大戰。因爲種種緣由,溥儀將寶匣藏於太極皇陵。抗戰期間,愛國人士崔二侉子帶領衆人深刻太極皇陵,盜走了覲天寶匣。此後的幾年裏,參與盜寶的人陸續神祕死亡,崔二侉子將寶匣交給偵探出身的蕭劍南。其後六十多年的時間裏,蕭劍南用了一輩子時間試圖尋找到事情的真相,直到臨終,纔將這件事告知本身的孫子蕭偉。蕭偉與好友高陽、趙穎試圖打開覲天寶匣……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位的

  出處:http://www.cnblogs.com/bigmonkey

  本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途! 

  掃描二維碼關注公衆號「我是8位的」

相關文章
相關標籤/搜索