七、动态规划
斐波那契数列
70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
思路: 假设要爬n=10级台阶,登上第10级台阶是通过两种走法得来:①从9级台阶走一级;②从8级台阶走两级的。而登上第9级台阶又是通过两种走法得来:①从8级台阶走一级;②从7级台阶走两级。不难发现这里存在重复子问题,其递推式为 f(n) = f(n-1) + f(n-2),并且f(1)和f(2)已知,f(1) = 1 上一级台阶只有一种走法;f(2) = 2 上两级台阶有两种走法。可以使用动态规划dp解决此问题。
代码实现:
class Solution:
def climbStairs(self, n: int) -> int:
# 动态规划dp
# 递推式:f(n) = f(n-1) + f(n-2)
methods = [0,1,2] # 最前面补个0是为了让 methods[1]=1, methods[2]=2,更直观
if n > 2:
for i in range(n-2):
m = methods[-1]+methods[-2]
methods.append(m)
return methods[n]
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 400
思路: 当只有一间房屋时,能偷到的最高金额就是偷这间屋子能得到的钱,当只有两间房屋时,由于房屋相邻不能同时偷窃,只能偷其中的一间房屋,因此选择两间中钱更多的偷窃。当有k间房时(k>2),有两个偷窃方案:1.偷第k间,此时不能偷k-1间,偷窃总金额为 第k间的钱+偷前k-2间所能获得的最大钱;2.不偷第k间,偷窃总金额为 偷前k-1间所能获得的最大钱。这两个方案取最大,就是偷k间房子所能获得的最大金额。动态规划:dp[k] = max(dp[k-2] + nums[k], dp[k-1])。
代码实现:
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0] # 偷第一家,偷到的就是第一家的钱
dp[1] = max(nums[0],nums[1]) # 偷前两家,偷到的就是两家之中最多的钱
for i in range(2,len(nums)):
# 第i间房。可以选择偷或者不偷,偷的话就是第i间的钱加上偷前面i-2间最高金额;不偷的话就是偷前面i-1间最高金额
dp[i] = max(dp[i-2] + nums[i], dp[i-1])
return dp[len(nums)-1]
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
思路: 在198题基础上增加了环形的条件,可以将本题的环状排列房间拆成两个单排排列的房间,即可用198的思路。1.不偷第一间房就可以偷最后一间房,即nums[1:];2.偷第一间房就不能偷最后一间房,即nums[:-1]。
代码实现:
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 0:
return 0
if len(nums) <= 3:
return max(nums)
def rob1(nums):
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0],nums[1])
for k in range(2,len(nums)):
dp[k] = max(nums[k] + dp[k-2], dp[k-1])
return dp
p1 = rob1(nums[1:])
p2 = rob1(nums[:-1])
return max(p1[-1],p2[-1])
信件错排
题目描述:有 N 个信 和 N 个信封,它们被打乱,求错误装信方式的数量(全装错)。
思路: 定义一个数组dp存储错误方式数量,dp[i]表示 i 个信全装错的方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据i,k是否相等,有两种情况:(1)i == k,交换 i 和 j 的信之后,他俩的信装对了位置,但其余 i-2 封信有 dp[i-2]种错误装信方式。由于 j 有 i-1 种取值,因此有 (i-1) * dp[i-2] 种错误装信方式;(2)i != k,交换 i 和 j 的信后,第 i 个信在正确位置,其余 i-1 封信有 dp[i-1] 种错误方式。由于 j 有 i-1 种取值,因此有 (i-1) * dp[i-1]种错误装信方式。 综上,dp[i] = (i-1) * dp[i-2] + (i-1) * dp[i-1]。
此题也可以用高中排列组合知识,用所有信件的装信方式 减去 装对的方式(有一个装对也叫装对),例如5封信时,所有装信方式为:5! = 120,装对的方式为:C(5,1) * 9 + C(5,2) * 2 + C(5,3) * 1 + 1 = 76,故5封信全装错一共有:120 - 76 = 44 种。
代码实现:
class Solution:
def wrong_mail(self,n):
if n == 0:
return 0
dp = [0] * (n+1)
dp[0] = 0
dp[1] = 0
dp[2] = 1
dp[3] = 2
for i in range(4, n+1):
dp[i] = (i-1)*dp[i-2] + (i-1)*dp[i-1]
return dp[n]
母牛生产
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
思路: 前三年只有初始母牛能生小牛,因为所有牛都不会死,所以第 N-1 年的所有牛 dp[N-1] 都会活到第N年;成熟的母牛每年生一头小牛,所以第 N-3 年中的所有牛到第N年都会各生一头小牛,一共dp[N-3]头;所以第N年牛的总数是:dp[N] = dp[N-1] + dp[N-3]。此题也可以列举出前几项之后找规律。
代码实现:
class Solution:
def cow_num(self, n):
if n <= 0:
return 0
if n <= 3:
return n
dp = [0] * (n+1)
dp[0] = 0
dp[1] = 1
dp[2] = 2
dp[3] = 3
for i in range(4, n+1):
dp[i] = dp[i-1] + dp[i-3]
return dp[n]
矩阵路径
64. 最小路径和
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
思路: 一开始想到dfs,但其实这是典型的动态规划。因为每个单元格只能从自己的左边单元格和自己的上方单元格走到。新建一个dp二维矩阵用于保存每一步的最小路径和,需要考虑四种情况:(1)没有左边和上边元素(grid[0][0]);(2)只有左边元素:dp[x][y] = dp[x][y-1] + grid[x][y];(3)只有上边元素:dp[x][y] = dp[x-1][y] + grid[x][y];(4)上边元素和左边元素都有:dp[x][y] = min(dp[x-1][y] + grid[x][y], dp[x][y-1] + grid[x][y])。最后返回dp[-1][-1]即可。更简单的方法是,不需要新建dp,直接遍历grid对其进行修改即可,因为遍历过的元素都在当前元素的左上方,不会再使用到,这样做可以节省空间,使空间复杂度变为:O(1)。
代码实现:
class Solution:
def minPathSum(self, grid):
if len(grid) == 0:
return 0
dp = [[0 for _ in range(len(grid[0]))] for _ in range(len(grid))]
for x in range(len(grid)):
for y in range(len(grid[0])):
# 遍历到grid[0][0]时,进入第一个if条件,由于dp每个元素都初始化为0,故dp[0][-1]==0,dp[0][0]==1
if x == 0: # 只有左边元素
dp[x][y] = dp[x][y-1] + grid[x][y]
elif y == 0: # 只有上边元素
dp[x][y] = dp[x-1][y] + grid[x][y]
else:
dp[x][y] = min(dp[x][y-1] + grid[x][y], dp[x-1][y] + grid[x][y])
# return dp[len(grid)-1][len(grid[0])-1]
return dp[-1][-1]
# 不开辟新的dp空间,直接覆盖原grid,空间复杂度为O(1)
class Solution:
def minPathSum(self, grid: [[int]]) -> int:
for i in range(len(grid)):
for j in range(len(grid[0])):
if i == j == 0: continue
elif i == 0: grid[i][j] = grid[i][j-1] + grid[i][j]
elif j == 0: grid[i][j] = grid[i-1][j] + grid[i][j]
else: grid[i][j] = min(grid[i-1][j], grid[i][j-1]) + grid[i][j]
return grid[-1][-1]
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 10 ^ 9
思路: 利用动态规划,构建二维列表dp,由于机器人每次只能向下或向右移动一步,所以二维数组的第一行和第一列都是1,其余的单元格是自己左边单元格与上边单元格的和,最后返回dp[-1][-1]。
排列组合法:由于机器人每次只能向下或向右移动一步,要走到右下角一定是向右走 m-1 步,向下走 n-1 步。也就是总共要走 m-1+n-1 == m+n-2 步,其中有 m-1 步是向右走的。这就是从 m+n-2 步中选择 m-1步向右的走法,即有:C(m+n-2, m-1) 中不同的走法。
代码实现:
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
if m == 0 or n == 0:
return 0
dp = [[0 for _ in range(m)] for _ in range(n)]
for x in range(n):
for y in range(m):
if x == 0 or y == 0:
dp[x][y] = 1
else:
dp[x][y] = dp[x-1][y] + dp[x][y-1]
return dp[-1][-1]
63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
说明:m 和 n 的值均不超过 100。
示例 1:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
思路: 本题在62题基础上还是使用动态规划,首先若网格不存在或者起点和终点上有障碍,直接返回0。在初始化上需要注意,dp[0][0] = 1,第一行若当前