回溯法與動態規劃例項分析
回溯法與動態規劃
1、回溯法
1.1 適用場景
回溯法很適合解決迷宮及其類似的問題,可以看成是暴力解法的升級版,它從解決問題每一步的所有可能選項裡系統地選擇出一個可行的解決方案。回溯法非常適合由多個步驟組成的問題,並且每個問題都有多個選項。當我們從一步選擇了其中一個選項時,就進入下一步,然後又面臨新的選項。我們就這樣重複選擇,直至到達最終的狀態(遞迴終止條件)。
1.2 過程:
用回溯法解決問題的所有選項可以形象地用樹狀結構表示。 (1)在某一步有n個可能的選項,那麼該步驟可以看成是樹狀結構的一個節點,每個選項看成樹中節點連線線,經過這些連線線到達該節點的n個子節點樹的葉節點對應著終結狀態。如果在葉節點的狀態滿足題目的得約束條件,那麼我們找到了一個可行解。 (2)如果在葉節點的狀態不滿足約束條件,那麼只好回溯到它的上一個節點再嘗試其它選項。如果上一個節點所有可能的選項都不能到達滿足約束條件的終結狀態,則再次回溯到上一個節點。如果所有節點的所有選項都已經嘗試過仍然不能到達滿足約束條件的終結狀態,則該問題無解。
1.3 優化
思想: 同一種思路基於迴圈和遞迴的不同實現,時間複雜度可能大不相同,因此,我們常常用自上而下的遞迴思路分析問題,然後基於自下而上的迴圈實現程式碼; 但是遞迴轉化為迴圈有時並不是很好理解,因此可以採用動態規劃的思想,在遞迴過程中開闢一塊快取,用於儲存已經計算過的重複子問題,在後續計算時可以直接使用,節省時間,這種方式也可以達到自下而上迴圈的效果,並且不用進行遞迴到迴圈的轉化。
解題思路: 這類問題主要包含三部分: (1) 化為子問題:一般是指化為樹的形式,比較直觀,容易理解; (2) 終止條件:樹的葉子節點作為終止條件; (3) 迭代公式:遞迴迴圈的過程。
2、動態規劃
參見:
3、例項分析
下面從四個演算法題來分析這類問題:
問題1:機器人的運動範圍
題目:地上有一個m行n列的方格。一個機器人從座標(0, 0)的格子開始移動,它每一次可以向左、右、上、下移動一格,但不能進入行座標和列座標的數位之和大於k的格子。例如,當k為18時,機器人能夠進入方格(35, 37),因為3+5+3+7=18。但它不能進入方格(35, 38),因為3+5+3+8=19。請問該機器人能夠到達多少個格子?
class Solution:
"""
計算機器人行走範圍
"""
def moving_count(self, threshold, rows, cols):
"""
外部呼叫函式,本函式呼叫遞迴函式,計算行走範圍
:param threshold: 座標數位之和的閾值
:param rows: 座標行數
:param cols: 座標列數
:return: 機器人行走範圍之和
"""
if threshold is None or rows < 1 or cols < 1:
return 0
visits = [0] * (rows * cols)
counts = self.moving_count_core(threshold, rows, cols, 0, 0, visits)
return counts
def moving_count_core(self, threshold, rows, cols, row, col, visits):
"""
遞迴計算機器人行走範圍
:param threshold: 座標數位之和的閾值
:param rows: 行數
:param cols: 列數
:param row: 開始行
:param col: 開始列
:param visits: 標識行走過的路徑
:return: 機器人行走範圍之和
"""
moving_count = 0
if self.check(threshold, rows, cols, row, col, visits):
visits[row*cols+col] = 1
# 連續行走,只需要一行遞迴語句,行走距離累加
moving_count = 1 + self.moving_count_core(threshold, rows, cols, row - 1, col, visits) \
+ self.moving_count_core(threshold, rows, cols, row, col - 1, visits) \
+ self.moving_count_core(threshold, rows, cols, row + 1, col, visits) \
+ self.moving_count_core(threshold, rows, cols, row, col + 1, visits)
return moving_count
def check(self, threshold, rows, cols, row, col, visits):
"""
檢查當前座標是否滿足要求,即座標的數位之和小於threshold
:param threshold: 座標數位之和的閾值
:param rows: 行數
:param cols: 列數
:param row: 開始行
:param col: 開始列
:param visits: 標識行走過的路徑
:return: 是否滿足條件
"""
if 0 <= row <rows and 0 <= col <cols and self.get_digit_sum(row) + self.get_digit_sum(col) <= threshold \
and visits[row*cols+col] == 0:
return True
return False
def get_digit_sum(self, number):
"""
獲得一個數字的數位之和
:param number: 數字
:return: 數位之和
"""
num_str = str(number)
num_sum = 0
for i in num_str:
num_sum += int(i)
return num_sum
問題2:矩陣中的最大遞增路徑
題目:給定一個整數矩陣,找到遞增最長路徑的長度。從每一個單元格,你可以向四個方向移動:左,右,上,下。不能向對角線移動或移動到邊界以外。
class Solution:
"""
給定一個整數矩陣,找到增加最長路徑的長度。
"""
def longest_increasing_path(self, matrix):
"""
對矩陣中的每一個座標計算最遠距離
:param matrix: 整數矩陣
:return: 最長路徑的長度
"""
rows = len(matrix)
cols = len(matrix[0])
# 動態規劃,用於儲存已計算的座標位置
lens = [[-1] * cols for _ in range(rows)]
max_path = 0
for row in range(rows):
for col in range(cols):
max_path = max(max_path, self.longest_increasing_path_core(matrix, rows, cols, row, col, lens))
return max_path + 1
def longest_increasing_path_core(self, matrix, rows, cols, row, col, lens):
"""
尋找四個方向上的最大路徑
:param matrix: 整數矩陣
:param rows: 行數
:param cols: 列數
:param row: 當前行
:param col: 當前列
:param lens: 動態規劃快取陣列
:return: 最大路徑長度
"""
# 利用快取節省計算時間
if lens[row][col] != -1:
return lens[row][col]
# 每個座標四個方向分別遞迴
left, right, up, down = 0, 0, 0, 0
if col - 1 >= 0 and matrix[row][col] < matrix[row][col-1]:
left = 1 + self.longest_increasing_path_core(matrix, rows, cols, row, col-1, lens)
if row - 1 >= 0 and matrix[row][col] < matrix[row-1][col]:
up = 1 + self.longest_increasing_path_core(matrix, rows, cols, row-1, col, lens)
if col + 1 < cols and matrix[row][col] < matrix[row][col+1]:
right = 1 + self.longest_increasing_path_core(matrix, rows, cols, row, col+1, lens)
if row + 1 < rows and matrix[row][col] < matrix[row+1][col]:
down = 1 + self.longest_increasing_path_core(matrix, rows, cols, row+1, col, lens)
# 求四個方向的最大路徑
lens[row][col] = max(max(left, right), max(up, down))
return lens[row][col]
比較本題與機器人行走問題: (1)機器人行走的範圍問題只需一行遞迴語句,因為機器人的行走範圍是持續累加的,即從頭到尾是一個問題。 (2)本題要找到遞增行走的最長路徑,需要找到四個方向中的最大路徑,因此需要四條遞迴語句(滿足一定條件),在四個方向上遞迴尋找,即每個節點存在多個選項,這裡可理解為尋找樹的 最大高度(左右孩子分別遞迴,然後返回最大值),本題為在四個方向上分別遞迴,然後返回最大值,使用動態規劃,即快取,可以提高計算效能;
問題3:最長陸地飛機場
題目:演練場的範圍為M*N,海平面高度為H,若演練場中的座標高度小於海平面高度且與邊緣相連, 則為海洋,其餘均為陸地,如下例中(0,3), (1,3)為海洋,(1,1)不與邊緣相連視為陸地。 在陸地上海拔高度持續降低的路徑可以作為飛機場,求出給定矩陣中可以作為飛機場的最長路徑。 比如:下例中最長路徑為13 -> 12 -> 11 -> 10 -> 9 -> 2 -> 0 輸入: 3 5 0 14 2 9 10 11 7 0 9 0 12 7 7 10 0 13 輸出: 7
class Solution:
def moving_count(self, matrix, rows, cols, H):
"""
對每個陸地座標計算其能達到的最遠距離
:param matrix: 整數矩陣
:param rows: 行數
:param cols: 列數
:param H: 海平面高度
:return: 最大遞減距離
"""
#
ocean = [[0] * cols for i in range(rows)]
for row in range(rows):
for col in range(cols):
if matrix[row][col] <= H:
is_ocean = self.verify(matrix, rows, cols, row, col, H)
if is_ocean:
ocean[row][col] = 1
# 動態規劃的快取
lens = [[-1] * cols for _ in range(rows)]
# 對矩陣的每一個座標計算最大路徑
max_path = 0
for row in range(rows):
for col in range(cols):
if ocean[row][col] == 0:
max_path = max(max_path, self.moving_count_core(matrix, rows, cols, row, col, lens, ocean))
return max_path + 1
# 這部分直接模仿問題二得到
def moving_count_core(self, matrix, rows, cols, row, col, lens, ocean):
"""
尋找四個方向上的最大路徑
:param matrix: 整數矩陣
:param rows: 行數
:param cols: 列數
:param row: 當前行
:param col: 當前列
:param lens: 快取陣列
:param ocean: 海洋座標標誌陣列
:return: 某座標可達到的最遠距離
"""
# 動態規劃
if lens[row][col] != -1:
return lens[row][col]
# 四個方向上遞迴
left, right, up, down = 0, 0, 0, 0
if col - 1 >= 0 and ocean[row][col - 1] == 0 and matrix[row][col] > matrix[row][col - 1]:
left = 1 + self.moving_count_core(matrix, rows, cols, row, col - 1, lens, ocean)
if row - 1 >= 0 and ocean[row - 1][col] == 0 and matrix[row][col] > matrix[row - 1][col]:
up = 1 + self.moving_count_core(matrix, rows, cols, row - 1, col, lens, ocean)
if col + 1 < cols and ocean[row][col + 1] == 0 and matrix[row][col] > matrix[row][col + 1]:
right = 1 + self.moving_count_core(matrix, rows, cols, row, col + 1, lens, ocean)
if row + 1 < rows and ocean[row + 1][col] == 0 and matrix[row][col] > matrix[row + 1][col]:
down = 1 + self.moving_count_core(matrix, rows, cols, row + 1, col, lens, ocean)
# 求每個座標開始的最遠距離
lens[row][col] = max(max(left, right), max(up, down))
return lens[row][col]
# 這部分參考劍指offer面試題12:矩陣中的路徑
def verify(self, matrix, rows, cols, row, col, H):
"""
判斷矩陣內的海洋座標
:param matrix: 整數矩陣
:param rows: 行數
:param cols: 列數
:param row: 當前行
:param col: 當前列
:param H: 海平面高度
:return: 當前座標是否海洋
"""
# 不滿足約束條件,回溯
if matrix[row][col] > H:
return False
# 滿足終止條件(與海洋相連,即該座標在矩陣邊界處),結束
if row == 0 or col == 0 or row == rows-1 or col == cols-1:
return True
# 向四個方向上尋找,只要一個方向上滿足終止條件,返回
is_ocean = self.verify(matrix, rows, cols, row, col - 1, H) \
or self.verify(matrix, rows, cols, row - 1, col, H) \
or self.verify(matrix, rows, cols, row, col + 1, H) \
or self.verify(matrix, rows, cols, row + 1, col, H)
return is_ocean
遞迴轉化為迴圈的例子:
問題四:硬幣組合問題
給你六種面額 1、5、10、20、50、100 元的紙幣,假設每種幣值的數量都足夠多,編寫程式求組成N元(N為0~10000的非負整數)的不同組合的個數。 輸入描述: 輸入包括一個整數n(1 ≤ n ≤ 10000) 輸出描述: 輸出一個整數,表示不同的組合方案數 輸入例子1: 1 輸出例子1: 1
def get_n(n, m, money_list):
"""
遞迴解法
:param n: 總錢數
:param m: 紙幣種類
:param money_list: 紙幣陣列
:return: 不同組合個數
"""
if n == 0:
return 1
if money_list[m] == 1:
return 1
if n >= money_list[m]:
count = get_n(n-money_list[m], m, money_list) + get_n(n, m-1, money_list)
else:
count = get_n(n, m-1, money_list)
return count
def get_n_dp(n, money_list):
"""
迴圈(動態規劃)
:param n: 總錢數
:param money_list: 紙幣陣列
:return: 不同組合個數
"""
dp = [1] * (n + 1)
for i in range(1, 6):
for j in range(0, n + 1):
if j >= money_list[i]:
dp[j] = dp[j] + dp[j - money_list[i]]
return dp[-1]