回溯法基本思想_LeetCode--回溯法心得
這兩天在刷LeetCode37題解數獨時,被這個回溯法折騰的不要不要的,于是我疼定思疼發誓一定要找個能解決這類回溯法的套路出來,方便以后快速解決此類題目。于是我在網上找了兩個很經典的回溯法題目--八皇后問題和迷宮問題,認真總結了一番,發現其中還真的有一些共同之處,我會在下面好好講述。
首先,回溯法所用到的核心思想就是遞歸法,雖然其過程邏輯很清楚,而且執行效率很高。但缺點也是與之對應的,邏輯清楚,必然其抽象性很高,所以有時候就是這樣的情況:看它的解題過程很容易看懂,但要是讓你自己動手寫這個遞歸過程,發現很難下筆。當時我就是這樣的情況,于是我想在網上找找看看能不能有哪位大佬分享一些解題心得供我們這些算法渣渣思考學習的,然而網上的一些csdn博客(這兒不是刻意貶低csdn的哈)寫的東西都是千篇一律,都是在把一些舊的不能再舊的東西炒來炒去,沒有一點營養!別人靠不住,那就只能靠自己唄!于是我自己找例子,自己琢磨,終于是有些小收獲,本著慈悲為懷的精神,故想拿出來供大家參考,避免大家少趟坑,保證不打馬虎眼,不炒現飯。當然了,若有不當之處,請各位筆友指出,面的誤人子弟。
1 八皇后問題
問題描述:
該問題是國際西洋棋棋手馬克斯·貝瑟爾于1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處于同一行、同一列或同一斜線上,問有多少種擺法。
看完問題描述后,大家之前要是熟悉此題目的也可以先動手做做,看看還能不能解出來。再正式回答此題目之前我還是先把我寫的答案貼出來,讓大家對整個處理過程有個大致的印象。
八皇后問題代碼如下:
# 檢測皇后之間的位置關系 def conflict(queen_str, current_queen):""":param queen_str: str-->指代當前皇后存放之前的所有皇后的集合:param current_queen: int-->指代當前皇后想要存放的位置:return:Flag: boolean-->指代當前位置的皇后是否與之前所有位置的皇后有沖突"""# 此處的queen_length既是之前保存的queen_list集合的長度,也可以理解為當前current_queen皇后的行下標queen_length = len(queen_str)# 定義是否有位置沖突的標簽Flag = Falsefor index in range(queen_length):# queen_length - index主要是控制相鄰兩行的皇后不能處于對角線上,其他的就沒要求if abs(current_queen-int(queen_str[index])) in(0, queen_length-index):Flag = Truebreakreturn Flag# 定義執行皇后問題的主函數 def queens(nums=8, queen_str=""):""":param nums: int-->指代整個棋盤中想要存放皇后的個數:param queen_str: str-->指代當前皇后存放之前的所有皇后的集合:return:final_queens: List[int]-->指代最后符合要求的皇后的位置"""final_queens = []# 定義遞歸函數,獲取所有八皇后的值def back(queen_str):# 出口條件if len(queen_str) == nums:final_queens.append(queen_str)returnfor index in range(nums):Flag = conflict(queen_str, index)# 如果當前位置的皇后是否與之前所有位置的皇后沒有沖突,則執行下述代碼if Flag is False:back(queen_str+str(index))back(queen_str)return final_queensif __name__ == "__main__":final_queens = queens()print(final_queens)print(len(final_queens))寫的應該還是比較清楚的,大家也可以再看看官方給的回溯法的描述
描述:
回溯法(探索與回溯法)是一種選優搜索法,又稱為試探法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇并不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。
其實我總結起來就3點:
1 出口。一個遞歸算法一定要有出口,否則就是一個死循環了。出口語句一般都挺好寫的,但 是出口語句該放在哪兒了,這個就是關鍵了,這兒容許我先賣個關子。
2 遞歸函數的參數。一般情況下,遞歸函數是要帶參數的,因為遞歸操作都是用來處理下一次的過程,如果沒有參數的話,那么就很難從下一次的操作回溯到當前操作了。這么說,可能會有點迷糊,別急,后面我會舉例子,這兒還是賣個關子。
3 遞歸函數的處理過程。這個自不必多說,重中之重,需要好好理解其過程
上面3點就是我總結的關于回溯法的關鍵點了,我覺得只要真正的把這3步吃透,一般的回溯法題目是ok的(這可不是我吹牛哈)下面我就這3點仔細講講,大家可要豎起耳朵通清楚了哈。
1 出口
關于這個出口條件,就像我上面說的,它的關鍵是出口語句放置的位置,因為這個語句其實挺好寫的,一般也就2-3行代碼,大多數人都能想出來。但我覺得大多數人苦惱的就是不知道該把它放在哪兒,我剛開始也是這樣,后面總結了2-3題之后,我發現了一個萬能規律,就是把出口語句放在遞歸函數的第一行就行,大家可以看看八皇后問題的遞歸函數back()以及迷宮問題的遞歸函數back(),我這兒就直接貼出來。
八皇后問題的遞歸函數back()
# 定義遞歸函數,獲取所有八皇后的值def back(queen_str):# 出口條件if len(queen_str) == nums:final_queens.append(queen_str)returnfor index in range(nums):Flag = conflict(queen_str, index)# 如果當前位置的皇后是否與之前所有位置的皇后沒有沖突,則執行下述代碼if Flag is False:back(queen_str+str(index))迷宮問題的遞歸函數back()
def back(position=start, pos_list=[start]):# 該遞歸函數的出口if position == final_position:route.append(pos_list)print("successful")returnpos_x = position[0]pos_y = position[1]for direction in walk_route:next_position = [pos_x+direction[0], pos_y+direction[1]]if isValid(nums, next_position):# 記住,這兒一定要用另一個list集合保存當前路線pos_list以及該路線下一個位置,方便回溯找到pos_list# 如果直接對pos_list添加next_position,則不能回溯找到之前的pos_listpos_list_copy = []pos_list_copy.extend(pos_list)pos_list_copy.append(next_position)nums[pos_x, pos_y] = 0back(next_position, pos_list_copy)# 如果沒有找到出口,則將當前上一個位置0重置為1,回溯nums[pos_x, pos_y] = 1大家一對比就很清楚的看到,出口語句都是寫在最前面的,其實最主要的就是不能把出口語句放在for和while循環語句里面,因為出口語句一定要方便整個函數退出,大家聽不懂的強行記住沒問題的,要是出了問題,也別來找我,啊哈哈哈哈哈。
2 遞歸函數的參數
這個遞歸函數的參數的設置也是有很大門道的,設置的好就很容易得到答案,否則弄大半天可能還是沒有一點反應。大家一定要記住一點:這個參數是隨著每一次的遞歸操作而發生改變的。而回溯法很關鍵的一點就是:如果當前操作行不通,如何回溯到上一步操作。大家繼續看上面貼的兩個遞歸函數的參數,會發現其參數都是要改變的,既然參數會發生改變,那么我們要如何保存其上一步操作的值呢?大家可以再細細看看上述兩個函數的傳值操作。
八皇后問題的傳值操作
for index in range(nums):Flag = conflict(queen_str, index)# 如果當前位置的皇后是否與之前所有位置的皇后沒有沖突,則執行下述代碼if Flag is False:back(queen_str+str(index))大家可以看到back(queen_str+str(index))這一步,其傳的參數就是queen_str+str(index) 其實想法就是不破壞當前參數的值,直接把當前值加上一個值(大家可以理解為定義了另一個非queen_str當前值的值給傳到下一次函數),只要不破壞當前值,函數就能回溯。這一步很關鍵,大家可以好好品味。
for index in range(nums):Flag = conflict(queen_str, index)# 如果當前位置的皇后是否與之前所有位置的皇后沒有沖突,則執行下述代碼if Flag is False:queen_str = queen_str+str(index)back(queen_str )如果大家還有些疑惑的話,可以再把傳值操作改成這樣試試,你會發現結果會大相徑庭的,這里就是破壞了當前值。
迷宮問題的傳值操作
if isValid(nums, next_position):# 記住,這兒一定要用另一個list集合保存當前路線pos_list以及該路線下一個位置,方便回溯找到pos_list# 如果直接對pos_list添加next_position,則不能回溯找到之前的pos_listpos_list_copy = []pos_list_copy.extend(pos_list)pos_list_copy.append(next_position)nums[pos_x, pos_y] = 0back(next_position, pos_list_copy)# 如果沒有找到出口,則將當前上一個位置0重置為1,回溯nums[pos_x, pos_y] = 1大家再可以參考迷宮操作的傳值操作理解。
關于參數,我還有一點就是強調:就是結果一定是要有一個全局參數來保存,這個全局參數不會隨著每一次的遞歸操作而隨時改變,它只是用來保存每一次遞歸操作成功時的結果,其它的不關它的事。你仔細看看這兩個程序也會發現:它們在一開始就定義了一個List空列表。大家也可以照搬的,凡是結果需要保存的題目90%以上就是要預先定義一個List空列表(不要問我這個90%數據是怎么得來的哈,問了我也不知道,哈哈哈哈哈)
八皇后問題的List空列表
# 定義執行皇后問題的主函數 def queens(nums=8, queen_str=""):""":param nums: int-->指代整個棋盤中想要存放皇后的個數:param queen_str: str-->指代當前皇后存放之前的所有皇后的集合:return:final_queens: List[int]-->指代最后符合要求的皇后的位置"""# 定義一個保存結果的List列表final_queens = []迷宮問題的List空列表
""" 迷宮問題,使用回溯法 """ def maze(nums, start):""":param nums: List[List[int]]-->指代所給的迷宮:param start: List[int X, Y]-->指代起始點位置:return: route: List[]"""# 定義最終路線的集合route = []3 遞歸函數的處理過程
這個過程是最關鍵的了,但是也很少有人能把它說清楚,當然也包括我。我想來想去,總結起來一句話就是:如果當前遞歸過程的處理參數符合要求,則執行相關賦值或其它操作,然后轉入下一次遞歸,如果下一次遞歸不能找到出口,則把之前相關賦值或其它操作重置為初始狀態。說的有些抽象,但我目前確實是說的這么樣了,還需要自己好好看幾個題目,好好做幾道題目才能理解這層意思。大家也可以好好看看下述迷宮問題的處理過程。
迷宮問題的處理過程
nums[pos_x, pos_y] = 0 back(next_position, pos_list_copy) # 如果沒有找到出口,則將當前上一個位置0重置為1,回溯 nums[pos_x, pos_y] = 12 迷宮問題
問題描述:
定義一個二維數組:
int maze[5][5] = {0, 1, 0, 0, 0,0, 1, 0, 1, 0,0, 0, 0, 0, 0,0, 1, 1, 1, 0,0, 0, 0, 1, 0,};它表示一個迷宮,其中的1表示墻壁,0表示可以走的路,只能橫著走或豎著走,不能斜著走,要求編程序找出從左上角到右下角的最短路線。
解題思路:
本題我才用的方法也是很常規的廣度優先搜索(BFS)也就是定義四個方向,即上下左右,對迷宮內每個可以通過的點執行該四個方向的操作。具體的操作我這兒就不說了,直接貼代碼吧!
迷宮問題代碼如下:
mport numpy as np# 檢查當前位置是否有效 # 如果當前位置為0,則表示不能通過; # 如果當前位置表示為1,則表示可以繼續通過 def isValid(nums, current_position):''':param nums: List[List[int]]-->指代所給的迷宮:param current_position: List[int X, Y]-->指代當前坐標點位置:return: boolean-->指代當前位置是否有效'''pos_x = current_position[0]pos_y = current_position[1]if pos_x in range(len(nums)) and pos_y in range(len(nums)) and nums[pos_x, pos_y] == 1:return Trueelse:return False""" 迷宮問題,使用回溯法 """ def maze(nums, start):""":param nums: List[List[int]]-->指代所給的迷宮:param start: List[int X, Y]-->指代起始點位置:return: route: List[]"""# 定義最終路線的集合route = []# 定義當前點上下左右移動方向的集合walk_route = [[-1, 0], [0, -1], [1, 0], [0, 1]]# 獲取迷宮的終點nums_length = len(nums)final_position = [nums_length-1, nums_length-1]def back(position=start, pos_list=[start]):# 該遞歸函數的出口if position == final_position:route.append(pos_list)print("successful")returnpos_x = position[0]pos_y = position[1]for direction in walk_route:next_position = [pos_x+direction[0], pos_y+direction[1]]if isValid(nums, next_position):# 記住,這兒一定要用另一個list集合保存當前路線pos_list以及該路線下一個位置,方便回溯找到pos_list# 如果直接對pos_list添加next_position,則不能回溯找到之前的pos_listpos_list_copy = []pos_list_copy.extend(pos_list)pos_list_copy.append(next_position)nums[pos_x, pos_y] = 0back(next_position, pos_list_copy)# 如果沒有找到出口,則將當前上一個位置0重置為1,回溯nums[pos_x, pos_y] = 1back()return routeif __name__ == "__main__":nums = [[1, 0, 0, 1, 0, 1], [1, 1, 1, 0, 1, 0], [0, 0, 1, 0, 1, 0], [0, 1, 1, 1, 0, 0], [0, 0, 0, 1, 1, 1],[1, 0, 0, 0, 1, 1]]nums = np.array(nums)print(nums)current_position = [0, 0]print(maze(nums, current_position))該講的我在八皇后問題那兒都講到了,本題大家就用作檢驗方法和思考作用吧。
3 解數獨
問題描述
編寫一個程序,通過已填充的空格來解決數獨問題。
一個數獨的解法需遵循如下規則:
空白格用 '.' 表示。
一個數獨。
答案被標成紅色。
Note:
- 給定的數獨序列只包含數字 1-9 和字符 '.' 。
- 你可以假設給定的數獨只有唯一解。
- 給定數獨永遠是 9x9 形式的。
思路:
這一題是LeetCode上面的第37題,確實是挺惡心的,當時做了兩三天。但如果你把它解答出來了,我覺得你對應回溯法的理解應該是差不多了。下面我還是直接貼代碼了,相關注解我都在代碼里寫的很清楚了,大家應該很容易看得懂!
代碼如下:
import numpy as npclass Solution(object):# 本題采用回溯法解決# 當時在area3x3檢查時栽了跟頭def solveSudoku(self, board):""":type board: List[List[str]]:rtype: void Do not return anything, modify board in-place instead."""# board = np.array(board)def back(board, position=[0, 0]):# 如果當前數獨中沒有空白元素'.',則說明查找成功了if position == [-1, -1]:print("successful")return True# 獲取當前位置的橫縱坐標pos_x = position[0]pos_y = position[1]# 獲取當前位置的值pos_value = board[pos_x][pos_y]if pos_value == '.':for index in range(1, 10):value = str(index)if self.isValid(board, position, value) is True:board[pos_x][pos_y] = valuenext_position = self.getNextPosition(board, position)if back(board, next_position) is True:return Trueelse:board[pos_x][pos_y] = '.'else:next_pos = self.getNextPosition(board, position)back(board, next_pos)return Falseback(board)return board# 獲取下一個有效點的坐標位置def getNextPosition(self, board, position):next_x = position[0]next_y = position[1]while board[next_x][next_y] != '.':next_y += 1if next_y >= len(board):next_x += 1next_y = 0if next_x not in range(len(board)) or next_y not in range(len(board)):return [-1, -1]return [next_x, next_y]# 判斷當前位置是否有效def isValid(self, board, position, value):""":param board: array[[]]-->指代所給的數獨列表:param position: List[int x, y]-->指代所給的當前位置:param value: str-->指代當前位置的值:return: boolean-->若返回為True,則表示當前位置有效;反之,則無效"""board = np.array(board)# 獲取當前位置的橫縱坐標pos_x = position[0]pos_y = position[1]# 獲取當前位置橫縱坐標所對應的每一行每一列元素pos_row = board[pos_x]pos_col = board[:, pos_y]# 如果當前位置的值value與其所在的每一行或者每一列的值重復,則表示當前值無效,返回Falseif value in pos_row or value in pos_col:return False# 獲取當前位置點所在的3x3區域的位置area3x3_x = pos_x//3*3area3x3_y = pos_y//3*3area3x3_batch = board[area3x3_x:area3x3_x+3, area3x3_y:area3x3_y+3]# 如果當前位置的值value與其所在的3x3區域的值重復,則表示當前值無效,返回Falseif value in area3x3_batch:return Falsereturn Trueif __name__ == "__main__":board = [['5', '3', '.', '.', '7', '.', '.', '.', '.'],['6', '.', '.', '1', '9', '5', '.', '.', '.'],['.', '9', '8', '.', '.', '.', '.', '6', '.'],['8', '.', '.', '.', '6', '.', '.', '.', '3'],['4', '.', '.', '8', '.', '3', '.', '.', '1'],['7', '.', '.', '.', '2', '.', '.', '.', '6'],['.', '6', '.', '.', '.', '.', '2', '8', '.'],['.', '.', '.', '4', '1', '9', '.', '.', '5'],['.', '.', '.', '.', '8', '.', '.', '7', '9']]result = Solution().solveSudoku(board)print(np.array(result))暫時就補充這么多了,如果有更好的想法,我也會及時補充的,當然了,各位讀者如果有更nice的解題套路也希望大家積極分享!!!
2019/3/19 19:13補充
昨天有位朋友給我出了道題,也是涉及回溯法的,我覺得很有意思,所以想分享出來。
題目如下:
思路:
因為這個方格是不規則的方格,所以我首先想到的是將它填充成一個規則方格,便于我們插值。 如圖所示:
之所以這樣,是因為我們就可以直接對每個方格內的元素的相鄰方格內元素作比較了。
如圖所示:
總結
以上是生活随笔為你收集整理的回溯法基本思想_LeetCode--回溯法心得的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 特斯拉门店:降价维权后销量翻倍!全国一天
- 下一篇: 支付宝五福新玩法!多余福卡可兑换金条和华