出處:http://www.cnblogs.com/bigmonkey
本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途!
掃描二維碼關注公衆號「我是8位的」
不少時候,咱們只須要找到問題的最優解,若是使用盲目搜索策略,就必須先找出全部解,再進一步比較哪一個是最優的,當在解空間十分龐大時,不免有些浪費體力的感受。這時候,不妨試試更高效的貪心策略。算法
貪心策略也叫貪心算法(greedy algorithm)或貪婪算法,是一種強有力的窮舉搜索策略,它經過一系列選擇來找到問題的最優解。在每一個決策點,它都會作出當時看來是最優的選擇,一旦選擇後就無需回溯。簡單來講,貪心策略是一種「步步爲營」的策略——只要作好眼前的每一步,就天然會在將來獲得最好的結果,而且作過的決策就是是最好的決策,無需再次檢查。編程
不少時候,貪心法並不能保證獲得最優解,它能獲得的是較爲接近最優解的較好解,所以貪心法常常被用來解決一些對結果精度要求不高的問題。緩存
一個小偷撬開了一個保險箱,發現裏面有N個大小和價值不一樣的東西,但本身只有一個容量是M的揹包,小偷怎樣選擇才能使偷走的物品總價值最大?數據結構
假設有5個物品A,B,C,D,E,它們的體積分別是3,4,7,8,9,價值分別是4,5,10,11,13,能夠用矩形表示體積,將矩形旋轉90°後表示價值:post
下圖展現了一個容量爲17的揹包的4中填充方式,其中有兩種方式的總價都是24:學習
揹包問題有不少重要的實應用,好比長途運輸時,須要知道卡車裝載物品的最佳方式。spa
咱們基於貪心策略去解決揹包問題:在取完一個物品後,找到填充揹包剩餘部分的最佳方法。對於一個容量爲M的揹包,須要對每一種類型的物品都推測一下,若是把它裝入揹包的話總價值是多少,依次遞歸下去就能找到最佳方案。這個方案的原理是,一旦作出了最佳選擇就無需更改,也就是說一旦知道了如何填充較小容量的揹包,則不管下一個物品是什麼,都無需再次檢驗已經放入揹包中的物品(已經放入揹包中的物品必定是最佳方案)。3d
首先定義物品的數據模型:code
1 class Goods: 2 ''' 物品的數據結構 ''' 3 def __init__(self, size, value): 4 ''' 5 :param size: 物品的體積 6 :param value: 物品的價值 7 ''' 8 self.size = size 9 self.value = value
而後使用fill_into_bag方法尋找最佳填充方案。該方法接收揹包容量和物品清單兩個參數,返回揹包最大價值和最佳填充方案:orm
1 def fill_into_bag(M, goods_list): 2 ''' 3 填充一個容量是 M 的揹包 4 :param M: 揹包的容量 5 :param goods_list: 物品清單,包括每種物品的體積和價值,物品互不相同 6 :return: (最大價值,最佳填充方案) 7 ''' 8 space = 0 # 揹包的剩餘容量 9 max = 0 # 揹包中物品的最大價值 10 plan = [] # 最佳填充方案 11 12 for goods in goods_list: 13 space = M - goods.size 14 if space >= 0: 15 # 在取完一個物品(goods)後,填充揹包剩餘部分的最佳方法 16 space_plan = fill_into_bag(space, goods_list) 17 if space_plan[0] + goods.value > max: 18 max = space_plan[0] + goods.value 19 plan = [goods] + space_plan[1] 20 21 return max, plan
最後能夠看看小偷應該怎樣填充揹包:
1 def paint(plan): 2 print('最大價值:' + str(plan[0])) 3 print('最佳方案:') 4 for goods in plan[1]: 5 print('\t大小:{0}\t價值:{1}'.format(goods.size, goods.value)) 6 7 if __name__ == '__main__': 8 goods_list = [Goods(3, 4), Goods(4, 5), Goods(7, 10), Goods(8, 11), Goods(9, 13)] 9 plan = fill_into_bag(17, goods_list) 10 paint(plan)
運行結果:
遺憾的是,fill_into_bag方法只能做爲一個簡單的試驗樣品,它犯了一個嚴重的錯誤——第二次遞歸會忽略上一次所作的全部計算!這將致使要花指數級的時間才能計算出結果。爲了把時間降爲線性,須要使用動態編程技術對其進行改進,把計算過的值都緩存起來,由此獲得了揹包問題的2.0版:
1 # 字典緩存,space:(max,plan) 2 sd = {} 3 def fill_into_bag_2(M, goods_list): 4 ''' 5 填充一個容量是 M 的揹包 6 :param M: 揹包的容量 7 :param goods_list: 物品清單,包括每種物品的體積和價值,物品互不相同 8 :return: (最大價值,最佳填充方案) 9 ''' 10 space = 0 # 揹包的剩餘容量 11 max = 0 # 揹包中物品的最大價值 12 plan = [] # 最佳填充方案 13 14 if M in sd: 15 return sd[M] 16 17 for goods in goods_list: 18 space = M - goods.size 19 if space >= 0: 20 # 在取完一個物品(goods)後,填充揹包剩餘部分的最佳方法 21 print(goods.size, space) 22 space_plan = fill_into_bag_2(space, goods_list) 23 if space_plan[0] + goods.value > max: 24 max = space_plan[0] + goods.value 25 plan = [goods] + space_plan[1] 26 # 設置緩存,M空間的最佳方案 27 sd[M] = max, plan 28 29 return max, plan
此次能夠快速運行了,固然,咱們並不想把這個算法告訴小偷。
騎士旅行(Knight tour)問題是另外一個關於國際象棋的話題:騎士能夠由棋盤上的任一個方格出發,若是每一個方格只能到達一次,它要如何走完全部的位置?騎士旅行曾在十八世紀初倍受數學家與拼圖迷的注意,具體何時被提出已不可考。
「騎士」的走法和吃子都和中國象棋的「馬」相似,遵循「馬走日」的原則,只不過沒有「蹩腿」的約束:
在國際象棋中,騎士的價值爲3,雖然不算高,卻靈活、易調動、易雙抽,從這一點看,它的價值不亞於皇后。
咱們依然使用8×8的二維列表存儲棋盤信息,用0表示方格的初始狀態。使用一個從1開始的計數器記錄騎士旅行的軌跡,每走一步,計數器加1,同把騎士到達的方格狀態設置爲計數器的值,這些數值就是騎士的旅程軌跡:
騎士從一個方格出發, 最多能夠向八個方向行進,怎樣方便地表示這八個方向呢?咱們都見識或棋譜,在棋譜上,把騎士能夠到達的八個方格依次編號:
這像極了平面直角座標系,能夠把棋盤外圍的列序號看做y軸的座標,行序號看做x軸的座標,這樣棋盤上的每個方格就能夠用一個二維向量表示,向量的第一個份量是行號,第二個份量是列號。這其實是把咱們熟知的直角座標系順時針旋轉了90°,目的是爲了可以更方便地用二維列表表示。
騎士的初始位置是(3,3),從這裏出發能夠到達的另外八個位置依次是:(2,1),(1,2),(1,4),(2,5),(4,5),(5,4),(5,2),(4,1)。它們與初始位置的差值是:(-1,-2),(-2,-1),(-2,1),(-1,2),(1,2),(2,1),(2,-1),(1,-2)。因爲向量是表示大小和方向的量,與具體位置無關,因此騎士從任意位置出發,加上差值向量後均可以到達另外八個位置(不考慮棋盤邊界)。以上圖爲例:
用一個列表存儲這些差值向量。騎士旅行的數據模型:
1 class KnightTour: 2 def __init__(self): 3 # 棋盤的行數和列數 4 self.row_num, self.col_num = 8, 8 5 # 方格的初始狀態 6 self.s_init = 0 7 # 棋盤 8 self.chess_board = [[self.s_init] * self.row_num for i in range(self.row_num)] 9 # 差值向量,表示騎士移動的八個方向 10 self.v_move = [(-1, -2), (-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2)] 11 # 計數器終點 12 self.max = self.row_num * self.col_num 13 # 解決方案 14 self.answer = None
大概最容易想到的旅行方法就是深度優先搜索,基本思慮和八皇后相似:騎士從一個位置開始,向一個方向探索,沒法繼續前進時就「悔棋」,嘗試下一個方向,若是計數器能累加到64,說明騎士能夠完成旅行:
1 import copy 2 3 class KnightTour: 4 …… 5 def enable(self, curr_board, x, y): 6 ''' 判斷x,y位置是否可走 ''' 7 # 邊界條件判斷 and x,y位置是否曾經到達過 8 return (0 <= x < self.col_num and 0 <= y < self.row_num) and curr_board[x][y] == self.s_init 9 10 def move(self, curr_board, x, y, count): 11 ''' 12 騎士從(x,y)位置開始旅行 13 :param curr_board: 當前棋盤 14 :param x: 起始位置行號 15 :param y: 起始位置列號 16 :param count: 當前計數 17 :return 18 ''' 19 # 找到一種方法就退出 20 if self.answer is not None: 21 return 22 # 若是已經走遍了全部方格,該問題解決 23 if count > self.max: 24 self.answer = curr_board 25 return 26 27 if self.enable(curr_board, x, y): 28 curr_board[x][y] = count 29 # 繼續旅行,分別探測八個方向 30 for v_x, v_y in self.v_move: 31 # 複製棋盤上的狀態, 以便回溯 32 bord = copy.deepcopy(curr_board) 33 self.move(bord, x + v_x, y + v_y, count + 1)
這裏x是方格的行序號,y是方格列序號。Enable方法用於判斷(x,y)是否超出的棋盤邊界,同時也檢查了騎士是否已經到訪過(x,y)。move方法以遞歸的方式向下一步探索。悔棋的回溯操做使用了複製棋盤狀態的方式,這須要大量的內存,它有一個經過更改方格狀態的代替版本:
1 def move2(self, x, y, count): 2 ''' 3 騎士從(x,y)位置開始旅行 4 :param x: 起始位置行號 5 :param y: 起始位置列號 6 :param count: 當前計數 7 :return 8 ''' 9 # 找到一種方法就退出 10 if self.answer is not None: 11 return 12 # 若是已經走遍了全部方格,該問題解決 13 if count > self.max: 14 self.answer = copy.deepcopy(self.chess_board) 15 return 16 17 if self.enable(self.chess_board, x, y): 18 self.chess_board[x][y] = count 19 # 繼續旅行,分別探測八個方向 20 for v_x, v_y in self.v_move: 21 self.move2(x + v_x, y + v_y, count + 1) 22 # 將該位置設爲初始值,以便悔棋 23 self.chess_board[x][y] = self.s_init
move2只使用了一個棋盤,爲了回到上一個方格,當騎士探索完八個方向後,須要將當前所在方格重置爲初始狀態。move2的改進僅僅是節省了一點內存,和move1並無本質的區別,它們在運行時都至關緩慢。騎士每到達一個位置後,都將向八個方向探索,棋盤上共有64個方格,探索的數量也會產生爆炸,所以咱們在找到一種方案後就立刻退出。
完整代碼:
1 import copy 2 3 class KnightTour: 4 def __init__(self): 5 # 棋盤的行數和列數 6 self.row_num, self.col_num = 8, 8 7 # 方格的初始狀態 8 self.s_init = 0 9 # 棋盤 10 self.chess_board = [[self.s_init] * self.row_num for i in range(self.row_num)] 11 # 差值向量,表示騎士移動的八個方向 12 self.v_move = [(-1, -2), (-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2)] 13 # 計數器終點 14 self.max = self.row_num * self.col_num 15 # 解決方案 16 self.answer = None 17 18 def start(self, x, y): 19 ''' 20 旅行開始 21 :param x: 起始位置行號 22 :param y: 起始位置列號 23 :return: 24 ''' 25 # self.move(self.chess_board, x, y, 1) 26 self.move2(x, y, 1) 27 28 def enable(self, curr_board, x, y): 29 ''' 判斷x,y位置是否可走 ''' 30 # 邊界條件判斷 and x,y位置是否曾經到達過 31 return (0 <= x < self.col_num and 0 <= y < self.row_num) and curr_board[x][y] == self.s_init 32 33 def move(self, curr_board, x, y, count): 34 ''' 35 騎士從(x,y)位置開始旅行 36 :param curr_board: 當前棋盤 37 :param x: 起始位置行號 38 :param y: 起始位置列號 39 :param count: 當前計數 40 :return 41 ''' 42 # 找到一種方法就退出 43 if self.answer is not None: 44 return 45 # 若是已經走遍了全部方格,該問題解決 46 if count > self.max: 47 self.answer = curr_board 48 return 49 50 if self.enable(curr_board, x, y): 51 curr_board[x][y] = count 52 # 繼續旅行,分別探測八個方向 53 for v_x, v_y in self.v_move: 54 # 複製棋盤上的狀態, 以便回溯 55 bord = copy.deepcopy(curr_board) 56 self.move(bord, x + v_x, y + v_y, count + 1) 57 58 def move2(self, x, y, count): 59 ''' 60 騎士從(x,y)位置開始旅行 61 :param x: 起始位置行號 62 :param y: 起始位置列號 63 :param count: 當前計數 64 :return 65 ''' 66 # 找到一種方法就退出 67 if self.answer is not None: 68 return 69 # 若是已經走遍了全部方格,該問題解決 70 if count > self.max: 71 self.answer = copy.deepcopy(self.chess_board) 72 return 73 74 if self.enable(self.chess_board, x, y): 75 self.chess_board[x][y] = count 76 # 繼續旅行,分別探測八個方向 77 for v_x, v_y in self.v_move: 78 self.move2(x + v_x, y + v_y, count + 1) 79 # 將該位置設爲初始值,以便悔棋 80 self.chess_board[x][y] = self.s_init 81 82 def display(self): 83 if self.answer is None: 84 print('No answers!') 85 return 86 87 for row in self.answer: 88 for c in row: 89 print('%4d' % c, end='') 90 print() 91 92 if __name__ == '__main__': 93 kt = KnightTour() 94 kt.start(7, 7) 95 kt.display()
若是騎士從(7, 7)出發,是可以完成旅行的:
騎士的初始位置和探測方向的順序都會對運算時間產生極大的影響,若是把起始位置改爲(0,0),那麼上面的程序將運行至關長的時間。
並非在全部棋盤都能完成旅行,在3×3的棋盤上,騎士永遠都沒法到達中心位置:
因爲每步試探的隨機性和盲目性,使得基於深度優先策略的盲目搜索效率低下。若是可以找到一種克服這種隨機性和盲目性的辦法,按照必定規律選擇前進的方向,則成功的可能性將大大增長。J.C. Warnsdorff在1823年提出一個聰明的解法:有選擇地走下一步,先將最難的位置走完,既然每一格早晚都要走到,與其把困難留在後面,不如先走困難的路,這樣後面的路纔會寬闊,成功的機會也增大。
爲了簡單起見,咱們的騎士先在5×5的棋盤上旅行。他的初始位置是(0,0),這也是旅途的第一站,用「①」表示:
騎士的下一站只可能有兩個,(1,2)和(2,1),用深色方格表示:
若是騎士的下一站是(1,2),那麼從(1,2)出發,再下一站可以到達(0,4),(2,4),(3,3),(3,1),(2,0)這5個位置,將數字5標記在(1,2)中,用於表示路的寬窄,數字越小,路越窄,表示這條路線越困難。若是從(2,1)出發,再下一站可以到達另外五個位置:
第二站的「寬度」都是5。咱們已經在圖5.13中爲八個方向編好了序號,從位於十點鐘方向的1號開始,按照順時針順序逐一探索,選擇最窄目的地當中的第一個做爲下一站。按照這種方式,這裏選擇(1,2)做爲下一站,併爲該方格標記序號:
接下來從位置②繼續探測,尋找最窄的第三站:
每一個方格只能到達一次,因此不能再回到①,這也是貪心法和深度優先搜索的重要緣由之一——在貪心法中,每一步決策都是當下最好的,一旦作出選擇就再也不回溯。從位置②出發,到達的最窄第三站是(0,4):
按照這種方式繼續向前探測,騎士最終可以順利完成旅程:
按照這種思路使用貪心策略編寫代碼:
1 class KnightTourGreedy: 2 def __init__(self): 3 # 棋盤的行數和列數 4 self.row_num, self.col_num = 8, 8 5 # 方格的初始狀態 6 self.s_init = 0 7 # 棋盤 8 self.chess_board = [[self.s_init] * self.row_num for i in range(self.row_num)] 9 # 差值向量,表示騎士移動的八個方向 10 self.v_move = [(-1, -2), (-2, -1), (-2, 1), (-1, 2), (1, 2), (2, 1), (2, -1), (1, -2)] 11 # 計數器終點 12 self.max = self.row_num * self.col_num 13 # 解決方案 14 self.answer = None 15 16 def enable(self, x, y): 17 ''' 判斷x,y位置是否可走 ''' 18 # 邊界條件判斷 and x,y位置是否曾經到達過 19 return 0 <= x < self.col_num and 0 <= y < self.row_num and self.chess_board[x][y] == self.s_init 20 21 def get_width(self, x, y): 22 ''' x,y位置的「寬度」,數值越小,後面的路越窄 ''' 23 # 若是(x, y)位置曾經達到過,返回9(比八個方向多1) 24 if self.enable(x, y) == False: 25 return 9 26 n = 0 27 for v_x, v_y in self.v_move: 28 if self.enable(x + v_x, y + v_y): 29 n += 1 30 return n 31 32 def find_min(self, x, y): 33 ''' 找到從(x,y)出發,路「最窄」的下一個位置(下一個位置可到達的「不曾到訪」方格數最少) ''' 34 min_x, min_y, min_n = -1, -1, 100 35 for v_x, v_y in self.v_move: 36 n = self.get_width(x + v_x, y + v_y) 37 if n < min_n: 38 min_x, min_y, min_n = x + v_x, y + v_y, n 39 return min_x, min_y 40 41 def move(self, x, y, count): 42 ''' 騎士從(x,y)位置開始旅行 ''' 43 # 找到一種方法就退出 44 if self.answer is not None: 45 return 46 # 若是已經走遍了全部方格,該問題解決 47 if count > self.max: 48 self.answer = self.chess_board 49 return 50 51 if self.enable(x, y): 52 self.chess_board[x][y] = count 53 # 找出八個方向中,路「最窄」的一個 54 next_x, next_y = self.find_min(x, y) 55 # 向路「最窄」的方向繼續前進 56 self.move(next_x, next_y, count + 1) 57 58 def start(self, x, y): 59 ''' 旅行開始 ''' 60 self.move(x, y, 1) 61 62 def display(self): 63 if self.answer is None: 64 print('No answers!') 65 return 66 67 for row in self.answer: 68 for c in row: 69 print('%4d' % c, end='') 70 print() 71 72 if __name__ == '__main__': 73 kt = KnightTourGreedy() 74 kt.start(0, 0) 75 kt.display()
KnightTourGreedy的基本數據模型、棋盤邊界判斷和打印方法都和KnightTour一致。get_width用於計算從(x,y)位置的寬度,數值越小,該位置後面的路越「窄」,越難以到達。
對於路的寬窄來講,最窄是0,表示無路可走;最大是8,能夠向8個方向前進(不能回到出發的位置)。爲了讓更便於find_min方法選擇「最窄」的路,若是(x,y)曾經到訪過,則(x,y)的寬度是9(能夠選擇大於8而且小於min_n初始值的任何數),從而保證曾經到訪過的方格必定寬於不曾到訪的方格,以使得find_min不會選中曾經到訪過的方格。move方法沒有任何回溯,只是簡單地向最窄的方向一步步走下去:
改爲8×8或16×16的大棋盤後,KnightTourGreedy也能夠快速得出結果:
對於一些更大的棋盤,KnightTourGreedy運行時可能會出現「RecursionError: maximum recursion depth exceeded in comparison」,這是因爲遞歸深度超過了Python的默認限制。解決這一問題有兩種方法,一種是經過sys.setrecursionlimit()修改遞歸的默認深度,另外一種是將遞歸改爲循環。
做者:我是8位的