搜索的策略(1)——盲目搜索


  早在1952年,克勞德·香農就已經是電子信息界的傳奇人物,但是對當時的普通大眾來說,他仍然是個陌生人。不過在即將開始的會展后,他就人盡皆知了。

  在會議展上,香農展示了一只木制的、帶有銅須的玩具老鼠,這只老鼠能夠在迷宮中穿梭,最終找到出口處的金屬硬幣。老鼠是通過試錯的方式探索迷宮的,通過胡須,它可以感知是否碰到了走不通的牆壁,如果正對的牆壁走不通,就會退到后一個格子,旋轉90°,繼續探測下一個方向。如果走通了迷宮,老鼠還能記住這條路線,在下一次直接完成任務。

  其實香農的老鼠並沒有那么智能,它僅僅是“記住”路線,而不是“認識”路線——在老鼠走通迷宮后,如果撤掉迷宮的牆壁,老鼠依然是按照上次的線路前進。香農可沒有把這一點告訴觀眾,對於觀眾來說,這只機器老鼠簡直是來自異次元的天物,是一個會思考的機器!

  這個其貌不揚的小老鼠在當年登上了《時代》和《生活》雜志的封面,有一期文章甚至用《這只老鼠比你聰明》為標題。貝爾實驗室的老板們對這只老鼠印象深刻,他們在沖動中甚至要把香農拉進貝爾電話公司的董事會。

盲目搜索

  從老鼠探索迷宮的行為可以看出,它使用的深度優先搜索,這是一種簡單而暴力的窮舉搜索,幾乎沒有任何神秘性可言——找到一條路就一直走下去,直到撞牆為止,然后回溯,繼續探索,我們將這種搜索策略稱為“盲目搜索”。

  盲目搜索就是我們常說的“蠻力法”,又叫非啟發式搜索。作為最先想到的一種所搜策略,盲目搜索是一種無信息搜索。之所以被稱為“盲目”,是因為這種搜索策略只是按照預定的策略搜索解空間的所有狀態,而不會考慮到問題本身的特性。我們熟悉的深度優先搜索和廣度優先搜索就是兩種典型的盲目搜索。

  盲目搜索的名字不太好聽,容易被扣上“性能低下”的帽子,通常在找不到解決問題的規律時使用,但凡能找到某些規律,就不會選擇蠻力法,可以說盲目搜索策略是最后的大招。遺憾的是,很多問題都沒有明顯的規律可循,很多時候我們不得不求助於蠻力法。同時,由於思路簡單,盲目搜索策略通常是被人們第一個想到的,對於一些比較簡單的問題,盲目搜索確實能發揮奇效。對於盲目策略來說,我們既鄙視它近似蠻力的“性能低下”,又膜拜它把一切托付給計算機的“節省腦細胞”。

  在這一章里,我們先對盲目所搜展開討論,看看計算機帶給我們的神奇的大招。同時我們也將看到,“盲目”也並非一味的蠻干,在加以改進后,算法也並非像我們想象的那樣“性能低下”。

八皇后問題

  在國際象棋中,皇后(Queen)是攻擊力最強的棋子,皇后可橫、直、斜走,且格數不限,吃子與走法相同,往往是棋局中制勝的決定性力量,少掉一個皇后往往意味着棋局告負。皇后模擬的是歐洲中世紀時,王室自皇后娘家借來的援軍,作為棋盤上最具威力的一子,皇后代表強大的援軍。

  國際象棋棋手馬克斯·貝瑟爾於1848年提出了一個問題:在8×8格的國際象棋棋盤上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,一共有多少種擺法?

解空間

  八皇后看起來不那么好對付,除了挨個嘗試之外沒什么太好的辦法了。如果不考慮皇后的互相攻擊,將八個皇后每行擺放一個,一共會有多少種擺法呢?

  這個問題實際是在回答要搜索的解空間,這也是問題的關鍵——盲目搜索並不是漫無目的的搜索,而是在一個固定的范圍內尋找答案。有時候解空間的范圍不太容易直接回答,此時的一種思路是將問題簡化,由簡單的問題入手,逐步歸納總結出最后的答案。我們不妨把問題規模縮小,把2個皇后擺放在2×2的棋盤上,看看一共有多少種擺法。

  為了敘述方便,我們把每一行的皇后都編上號,擺放在第1行的皇后是1號,第2行是2號,兩個皇后一共有22=4種擺法:

  類似地,把3個皇后擺放在3×3的棋盤上時,先固定前兩行的皇后,再擺放3號,此時有3種擺法:

  保持1號不動,移動2號時,會產生另外6種擺法:

  這就形成了9種擺法。最后再移動1號,會產生另外9×2=18種擺法,因此把3個皇后擺放在3×3的棋盤上一共有33=27種擺法。以此類推,八皇后問題的解空間是88=16777216。僅僅是8個棋子就產生了如此多的解空間,沒有計算機可真是累人。

搜索策略

  160多萬的解空間真的要全部搜索嗎?當然不會,每個皇后都有自己的攻擊范圍,在擺放第1個皇后時,其它皇后的擺放位置也被某種程度的限定了:

  為了避開皇后1的攻擊范圍,第2個皇后只能在剩下的6淺色格子中選擇;而2號皇后落子后,又將對其它皇后做出進一步限制;到了第3行,擺放位置可能只剩下4個:

  我們並不會傻乎乎的對所有解空間進行搜索,而是隨着步驟的進行,避開了絕對不可能的解,從而有效地縮小了解空間的范圍。

  之后的皇后也采用這樣的辦法來擺放,這是一種試探法——先把皇后擺放在“安全”位置,然后設置她的攻擊范圍,再在下一個安全位置擺放下一個皇后;如果下一個皇后沒有“安全”位置了,那么“悔棋”,重新擺放上一皇后;再不行就“大悔棋”,上上一個皇后也重新擺放:

  這種帶回溯的方法就是我們熟知的深度優先搜索——只管埋頭前進,撞到牆才后退。

  雖然我們知道怎么擺放棋子,但計算機並不知道,在編寫代碼之前必須先完成從現實世界到軟件設計的映射。

  對於棋盤問題,一個有效的數據結構是8×8的二維列表,列表中的每個元素代表棋盤上的一個方格;方格有三種狀態,閑置、落子、是否處於被攻擊狀態,分別用0、1、2表示,這些構成了八皇后問題的數據模型。

  接下來是擺放棋子的行為。我們按行來擺放,每行擺放一個皇后,每次落子后都將把棋盤的部分方格設置為“被攻擊”。代碼:

 1 import copy
 2 
 3 class EightQueen:
 4     def __init__(self):
 5         # 棋盤單元格的初始狀態, 被皇后占據狀態, 被皇后攻擊狀態
 6         self.s_init, self.s_queen, self.s_attack = 0, 1, 2
 7         # 棋盤的行數和列數
 8         self.row_num, self.col_num  = 8, 8
 9         # 棋盤
10         self.chess_board = [[self.s_init] * self.row_num for i in range(self.row_num)]
11         # 解決方案列表
12         self.answer_list = []
13 
14     def start(self):
15         self.put_down(self.chess_board, 0)
16 
17     def put_down(self, curr_board, row):
18         ''' 在棋盤的第row行落子 '''
19         if row == self.row_num:
20             self.answer_list.append(curr_board)
21             return
22 
23         for col in range(self.col_num):
24             if self.enable(curr_board, row, col):
25                 # 復制棋盤上的狀態, 以便回溯
26                 bord = copy.deepcopy(curr_board)
27                 # 將皇后放在第row行第col列中
28                 bord[row][col] = self.s_queen
29                 # 設置第row行第col列的皇后的攻擊范圍
30                 self.set_attack(bord, row, col)
31                 # 繼續在下一行落子
32                 self.put_down(bord, row + 1)
33 
34     def enable(self, curr_board, row, col):
35         '''是否可以在棋盤的第row行第col列落子'''
36         return curr_board[row][col] == self.s_init
37 
38     def set_attack(self, curr_board, row, col):
39         '''設置第row行第cell列的皇后的攻擊范圍'''
40         # 最后一行沒有必要設置攻擊范圍
41         if row == self.row_num - 1:
42             return
43 
44         # 正下方的攻擊范圍
45         for next_row in range(row + 1, self.row_num):
46             curr_board[next_row][col] = self.s_attack
47         # 左斜下的攻擊范圍
48         left_col = col - 1
49         for next_row in range(row + 1, self.row_num):
50             if left_col >= 0:
51                 curr_board[next_row][left_col] = self.s_attack
52                 left_col -= 1
53             else:
54                 break
55         # 右斜下的攻擊范圍
56         right_col = col + 1
57         for next_row in range(row + 1, self.row_num):
58             if right_col < self.col_num:
59                 curr_board[next_row][right_col] = self.s_attack
60                 right_col += 1
61             else:
62                 break
63 
64     def display(self):
65         '''打印所有方案'''
66         length = len(self.answer_list)
67         if length == 0:
68             print('No answers!')
69             return
70 
71         print('There are %d answers!' % length)
72         for i in range(0, length):
73             print('-' * 20, 'answer', i + 1, '-' * 20)
74             bord = self.answer_list[i]
75             for row in bord:
76                 for c in row:
77                     if c == self.s_queen:
78                         print('%4d' % 1, end='')
79                     else:
80                         print('%4d' % 0, end='')
81                 print()
82 
83 if __name__ == '__main__':
84     eq = EightQueen()
85     eq.start()
86     eq.display()

  在set_attack()中,由於是逐行落子,所以只需要處理位於皇后下方的單元格。每次set_attack后,棋盤上的安全位置就又少了一些,下次落子只能落在下一行的“安全”位置上。

  enable()用來判斷是否安全,方法很簡單,只需檢查方格的狀態是否是初始狀態。

  在put_down()中,我們以一種“順序”的方式逐行落子,如果正好擺滿了八個皇后,則該種擺法是八皇后問題的一個解;如果沒有任何“安全”位置能夠擺放下一個皇后,則進行“悔棋”操作。每落一子都要記住棋盤的狀態,只有這樣才能回溯,以便進行“悔棋”。每落一子都相當於在解空間內進行了一次搜索,如果加入計數器的話,會發現最終只進行了15720次搜索,這可比之前少了兩個數量級。

  一共有92種解,其中一種:

  高斯認為八皇后問題有76種方案,1854年在柏林的象棋雜志上不同的作者發表了40種不同的解。看來沒有計算機的幫助,帶有窮舉性質的盲目搜索還真是一種不可嘗試的方法。

同根同源的另一種方法

  在EightQueen中,我們的方案是每次落子都重置棋盤的“安全”狀態,與之對應的另一種思路是“先檢查,再落子”,從而省去了方格的“被攻擊”狀態。

  仍然是按行來擺放,每行擺放一個皇后,擺放前需要檢查待擺放的棋子是否處於其它皇后的攻擊范圍內,只有不在攻擊范圍內時才允許擺放,否則“緩棋”,重新擺放上一個皇后,代碼如下:

 

 1 import copy
 2 
 3 class EightQueen_2:
 4     def __init__(self):
 5         # 棋盤單元格的初始狀態, 被皇后占據狀態
 6         self.s_init, self.s_queen = 0, 1
 7         # 棋盤的行數和列數
 8         self.row_num, self.col_num = 8, 8
 9         # 棋盤
10         self.chess_board = [[self.s_init] * self.row_num for i in range(self.row_num)]
11         # 解決方案列表
12         self.answer_list = []
13 
14     def start(self):
15         self.put_down(self.chess_board, 0)
16 
17     def put_down(self, curr_board, row):
18         ''' 在棋盤的第row行落子 '''
19         if row == self.row_num:
20             self.answer_list.append(curr_board)
21             return
22 
23         for col in range(self.col_num):
24             # 是否會和已經在棋盤上的皇后互相攻擊
25             if self.enable_attacked(curr_board, row, col) == False:
26                 # 復制棋盤上的狀態, 以便回溯
27                 bord = copy.deepcopy(curr_board)
28                 # 將皇后放在第row行第col列中
29                 bord[row][col] = self.s_queen
30                 # 繼續在下一行落子
31                 self.put_down(bord, row + 1)
32 
33     def enable_attacked(self, curr_board, row, col):
34         ''' 第row行第col列的皇后是否會和已經在棋盤上的皇后互相攻擊 '''
35         # 是否會和第col列的皇后互相攻擊
36         for last_row in range(row - 1, -1, -1):
37             if curr_board[last_row][col] == self.s_queen:
38                 return True
39         # 是否會和左斜上的皇后互相攻擊
40         left_col = col - 1
41         for last_row in range(row - 1, -1, -1):
42             if left_col >= 0 and curr_board[last_row][left_col] == self.s_queen:
43                 return True
44             left_col -= 1
45         # 是否會和右斜上的皇后互相攻擊
46         right_col = col + 1
47         for last_row in range(row - 1, -1, -1):
48             if right_col < self.col_num and curr_board[last_row][right_col] == self.s_queen:
49                 return True
50             right_col += 1
51 
52         return False
53 
54     def display(self):
55         '''打印所有方案'''
56         length = len(self.answer_list)
57         if length == 0:
58             print('No answers!')
59             return
60         print('There are %d answers!' % length)
61         for i in range(0, length):
62             print('-' * 20, 'answer', i + 1, '-' * 20)
63             bord = self.answer_list[i]
64             for row in bord:
65                 for c in row:
66                     print('%4d' % c, end='')
67                 print()
68 
69 if __name__ == '__main__':
70     eq = EightQueen_2()
71     eq.start()
72     eq.display()

  enable_attacked()用於判斷待擺放的棋子是否處於其它皇后的攻擊范圍內,由於是逐行落子,下方沒有皇后,所以只需要考慮上方的皇后即可

  EightQueen_2僅僅是用比較代替了修改,和EightQueen並沒有本質的區別,只是因為EightQueen更符合人類的行為,所以看起來也更高級一點。

  


   作者:我是8位的

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

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

  掃描二維碼關注公眾號“我是8位的”


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM