62 - 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
解法一:递归
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
if n == 1 or m == 1:
return 1
return self.uniquePaths(m-1, n) + self.uniquePaths(m, n-1)
很不幸超时了,超时样例是m=23,n=12.
递归的思路很简单,机器人从(1,1)走到(m,n)的不同路径数 = 机器人从(1,1)走到(m-1,n)的不同路径数 + 机器人从(1,1)走到(m,n-1)的不同路径数。而递归的base情况就是 m=1 或者 n=1. 因为此时只需要一直向右走或者一直向下走就可以了,都是只有一种情况,所以返回 1.
动态规划
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
#维护一个表格table,table的第(i,j)个位置记录了机器人走到当前位置的不同路径数
table = [[0 for _ in range(n)] for _ in range(m)]
#先把第一行和第一列填好,因为这些位置的路径数都是1
for i in range(m):
table[i][0] = 1
for j in range(1, n):
table[0][j] = 1
#开始填表格
for i in range(1, m):
for j in range(1, n):
table[i][j] = table[i-1][j] + table[i][j-1]
return table[m-1][n-1]
动态规划其实和递归的想法是一样的,核心点都在于想清楚:机器人从(1,1)走到(m,n)的不同路径数 = 机器人从(1,1)走到(m-1,n)的不同路径数 + 机器人从(1,1)走到(m,n-1)的不同路径数。这就是状态转移方程:table[i][j] = table[i-1][j] + table[i][j-1]. 不同在于递归是倒着往回推,动态规划是从初始情况往后推。注意 table[i-1][j] 和 table[i][j-1] 都要在 table[i][j] 前被遍历到。
70 - 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
if n == 2:
return 2
return self.climbStairs(n-1) + self.climbStairs(n-2)
因为一次可以走1或2步,因此 n级台阶的走法 = n-1级台阶的走法(最后走一步) + n-2级台阶的走法(最后走两步)
递归超时了。因为递归比循环慢很多,下面改写成循环的解法。
class Solution:
def climbStairs(self, n: int) -> int:
s1 = 1
s2 = 2
for i in range(3, n+1):
if s1 < s2:
s1 = s1 + s2
else:
s2 = s1 + s2
if n == 1:
return 1
elif n == 2:
return 2
else:
return max(s1, s2)
注意返回时分情况讨论。
78 - 子集
给你一个整数数组 nums ,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。
示例一:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例二:
输入:nums = [0]
输出:[[],[0]]
分析:
这一题和全排列不同,数组元素的顺序不能换,不然就会导致重复的情况。
本题解法参考了简单问题细致分析,两种DFS回溯思路
解法一:定义递归函数找到当前index开始的数组的所有子集
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
ans = []
def backtrack(index, tmp):
ans.append(tmp)
for i in range(index, len(nums)):
backtrack(i+1, tmp + [nums[i]])
backtrack(0, [])
return ans
这个递归比较有意思的是,没有一个显式定义的出口,而是通过循环控制递归结束。即 for 循环完成,当前层次的递归就结束了。回溯还是通过递归完成的,因为递归完成一次相当于入栈出栈一次,出栈即回溯了。
那么此处的递归函数backtrack到底是什么意思呢,它实现了以当前index为开头的所有情况,即数组 nums[index, len(nums)] 的所有子集。
以[1,2,3]为例,此解法元素的添加顺序是:[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]
解法二:对数组的每个元素考虑选或不选
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
ans = []
def backtrack(index, tmp):
if index == len(nums):
#定义了递归的base情况,当nums中的所有数字都被考虑过一遍时,return
return ans.append(tmp)
#子集中选择当前nums[index]
backtrack(index+1, tmp + [nums[index]])
#子集中不选择当前nums[index]
backtrack(index+1, tmp)
backtrack(0, [])
return ans
想法非常简单,对于每个元素都考虑选进子集和不选进子集两种情况,一次遍历数组中的所有元素,就会形成一棵树,最终的所有情况是树的所有叶子结点。我感觉这个解法里回溯的意味很浅,主要是递归,不过递归本质上就是回溯了。
以[1,2,3]为例,此解法元素的添加顺序是:[1,2,3], [1,2], [1,3], [1], [2,3], [2], [3], []