回溯法很适合解决迷宫及其类似的问题,可以看成是暴力解法的升级版,它从解决问题每一步的所有可能选项里系统地选择出一个可行的解决方案。回溯法非常适合由多个步骤组成的问题,并且每个问题都有多个选项。当我们从一步选择了其中一个选项时,就进入下一步,然后又面临新的选项。我们就这样重复选择,直至到达最终的状态(递归终止条件)。
用回溯法解决问题的所有选项可以形象地用树状结构表示。
(1)在某一步有n个可能的选项,那么该步骤可以看成是树状结构的一个节点,每个选项看成树中节点连接线,经过这些连接线到达该节点的n个子节点树的叶节点对应着终结状态。如果在叶节点的状态满足题目的得约束条件,那么我们找到了一个可行解。
(2)如果在叶节点的状态不满足约束条件,那么只好回溯到它的上一个节点再尝试其它选项。如果上一个节点所有可能的选项都不能到达满足约束条件的终结状态,则再次回溯到上一个节点。如果所有节点的所有选项都已经尝试过仍然不能到达满足约束条件的终结状态,则该问题无解。
思想:
同一种思路基于循环和递归的不同实现,时间复杂度可能大不相同,因此,我们常常用自上而下的递归思路分析问题,然后基于自下而上的循环实现代码;
但是递归转化为循环有时并不是很好理解,因此可以采用动态规划的思想,在递归过程中开辟一块缓存,用于存储已经计算过的重复子问题,在后续计算时可以直接使用,节省时间,这种方式也可以达到自下而上循环的效果,并且不用进行递归到循环的转化。
解题思路:
这类问题主要包含三部分:
(1) 化为子问题:一般是指化为树的形式,比较直观,容易理解;
(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
完整程序:
robot_run_range
   \;
问题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)本题要找到递增行走的最长路径,需要找到四个方向中的最大路径,因此需要四条递归语句(满足一定条件),在四个方向上递归寻找,即每个节点存在多个选项,这里可理解为寻找树的
最大高度(左右孩子分别递归,然后返回最大值),本题为在四个方向上分别递归,然后返回最大值,使用动态规划,即缓存,可以提高计算性能;
完整程序:
longest_increasing_path
   \;
问题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
完整程序:
airport_longest_moving_count
剑指offer面试题12:矩阵中的路径
   \;
递归转化为循环的例子:
问题四:硬币组合问题
给你六种面额 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]
完整程序:
select_money