-
本文整理了Lintcode动态规划类型题目的coding练习,参考了九章算法课程的配套习题,题目顺序由易到难,非常典型。算法入门的同学可以按照这个题目顺序进行练习。
-
代码都是用python实现,基本上是参考了九章用的解法,如果发现有错误或者不理解的地方,可以留言告诉我。
-
体会:
动态规划是一种思想,解决问题的一种思路,不是一种算法。就像递归是一种思想一样。递归是把大的问题拆分成小的问题来解决,小的问题解决后,大的问题自然也解决了。
动规的关键是判断一个题目是不是动规的,并且正确列出来状态转移方程,并且进行初始化。
怎么判断一个题目是不是动规呢?动规题目的常见特征,求最大、最小、是否可行,方案个数,极有可能是动规题目。求所有具体的方案,而不是方案的个数,或者输入是一个集合而不是序列,极不可能是动规。
坐标型动规和序列型动规是比较常见的题目,一定要掌握。状态方程中,描述是前i个····是序列型动规,包括单序列型和双序列型。
动态规划要用双重循环来实现。 -
目前在持续更新中
114. 不同的路径
有一个机器人位于一个 m × n 个网格左上角。
机器人每一时刻只能向下或者向右移动一步。机器人试图达到网格的右下角。
问有多少条不同的路径?
class Solution:
"""
@param m: positive integer (1 <= m <= 100)
@param n: positive integer (1 <= n <= 100)
@return: An integer
"""
def uniquePaths(self, m, n):
# write your code here
# 体会动态规划的思路,先从简单的题目来
# 状态方程,mp[i][j] = mp[i-1][j] + mp[i][j-1]
# 即从起始位置到达[i][j]的路径个数等于到达点[i-1][j]和点[i][j-1]的路径个数和
# 这个状态方程还比较好理解
# 起点,mp[0][0] = 1,即从起始位置到[0][0]只有一个路径
mp = {} # 定义二位数组是不是比较麻烦呢
# 先把左边框和上边框的点处理一下,省的下面分类讨论
for i in range(m):
mp[(i, 0)] = 1
for j in range(n):
mp[(0, j)] = 1
for i in range(m):
for j in range(n):
if i != 0 and j != 0:
mp[(i, j)] = mp[(i - 1, j)] + mp[(i, j - 1)]
return mp[(m - 1, n - 1)]
二刷
简单的坐标型动规问题,思路清楚,解法熟练。这里注意二位数组初始化问题就好了。
class Solution:
"""
@param m: positive integer (1 <= m <= 100)
@param n: positive integer (1 <= n <= 100)
@return: An integer
"""
def uniquePaths(self, m, n):
# write your code here
# 这是二刷,坐标型动规问题
# f[i][j]表示从起点到该点的路径数
# f[i][j] = f[i][j - 1] + f[i - 1][j]
# 起点f[0][0] = 0
# 对于二维数组问题,先初始化[0][j]和[i][0]的状态
f = [[0 for col in range(n)] for row in range(m)]
for i in range(1, m):
f[i][0] = 1
for j in range(1, n):
f[0][j] = 1
f[0][0] = 0
for i in range(1, m):
for j in range(1, n):
f[i][j] = f[i][j -1] + f[i - 1][j]
return f[m - 1][n - 1]
三刷,掌握很好
115. 不同的路径 II
114题目的follow up,现在考虑网格中有障碍物,那样将会有多少条不同的路径?
网格中的障碍和空位置分别用 1 和 0 来表示。
class Solution:
"""
@param obstacleGrid: A list of lists of integers
@return: An integer
"""
def uniquePathsWithObstacles(self, obstacleGrid):
# write your code here
# 基本理解了114题目,这是它的follow up
# 因为有了obstacleGrid,现成的二位数组,不用自己定义字典了
# 两个题目的基本思路一样,但是在实现细节上有挺多不同
# 好好体会这个题目的具体做法
mp = obstacleGrid
m = len(obstacleGrid)
n = len(obstacleGrid[0])
# 起点
mp[0][0] = 1 if mp[0][0] != 1 else 0
# 还是先处理左边框和上边框,这里不小心的话,容易出错
for i in range(1, m):
mp[i][0] = mp[i -1][0] if mp[i][0] != 1 else 0
for j in range(1, n): # 这里可能有个问题,就是重复处理了mp[0][0]这个点
mp[0][j] = mp[0][j - 1] if mp[0][j] != 1 else 0
for i in range(m):
for j in range(n):
if i != 0 and j != 0:
mp[i][j] = mp[i - 1][j] + mp[i][j - 1] if mp[i][j] != 1 else 0
return mp[m - 1][n - 1]
二刷,思路基本清楚
class Solution:
"""
@param obstacleGrid: A list of lists of integers
@return: An integer
"""
def uniquePathsWithObstacles(self, obstacleGrid):
# write your code here
# 典型的坐标型动规,稍微有点复杂
# dp[i][j]表示···
# dp[i][j] = d[i][j-1] + dp[i-1][j]
# 但是如果[i][j]等于1的话,dp[i][j]=0
if obstacleGrid[0][0] == 1:
return 0
m = len(obstacleGrid)
n = len(obstacleGrid[0])
dp = [[0] * n for _ in range(m)]
dp[0][0] = 1
for i in range(1, n):
if obstacleGrid[0][i] == 1:
continue
dp[0][i] = dp[0][i - 1]
for i in range(1, m):
if obstacleGrid[i][0] == 1:
continue
dp[i][0] = dp[i - 1][0]
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 1:
continue
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m - 1][n - 1]
三刷,掌握的不是很好
111. 爬楼梯
假设你正在爬楼梯,需要n步你才能到达顶部。但每次你只能爬一步或者两步,你能有多少种不同的方法爬到楼顶部?
class Solution:
"""
@param n: An integer
@return: An integer
"""
def climbStairs(self, n):
# write your code here
# 我小学和中学时也没参加过数学竞赛啥的,
# 感觉这个题目有点小学竞赛题的味道
# 我也没思路,看了答案后觉得挺简单
# 即可以一步到达最后一节楼梯或者两步达到最后一节楼梯
if n == 0:
return 0
if n <= 2:
return n
result = [1, 2]
for i in range(3, n + 1):
result.append(result[-1] + result[-2])
return result[-1]
二刷,掌握
110. 最小路径和
给定一个只含非负整数的m*n网格,找到一条从左上角到右下角的可以使数字和最小的路径。
class Solution:
"""
@param grid: a list of lists of integers
@return: An integer, minimizes the sum of all numbers along its path
"""
def minPathSum(self, grid):
# write your code here
# 九章视频课上讲过数字三角形的题目,自己琢磨了好一会
# 所以看到这个题目后不觉得很难,自己尝试做一做
# 从起始点到[i][j]的最小路径和f[i][j] = min(f[i][j-1], f[i-1][j]) + A[i][j]
# 起点f[0][0] = A[0][0]
m = len(grid)
n = len(grid[0])
minPathSum = grid
minPathSum[0][0] = grid[0][0]
# 处理左边框和上边框
for i in range(1, m):
minPathSum[i][0] = minPathSum[i - 1][0] + grid[i][0]
for j in range(1, n):
minPathSum[0][j] = minPathSum[0][j - 1] + grid[0][j]
for i in range(m):
for j in range(n):
if i != 0 and j != 0:
minPathSum[i][j] = min(minPathSum[i][j-1], minPathSum[i-1][j]) + grid[i][j]
return minPathSum[m-1][n-1]
二刷,思路清晰
三刷,完成的很好
109. 数字三角形
给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。
class Solution:
"""
@param triangle: a list of lists of integers
@return: An integer, minimum path sum
"""
def minimumTotal(self, triangle):
# write your code here
# 九章视频课对这个题目讲解了好几种方法,并分析了时间复杂度
# 我目前还没有完全消化,先实现动态规划的解法
# 后续要好好体会递归的解法,记忆化搜索的解法。
# f[i][j]是起始点到[i][j]的最小路径,f[i][j] = min(f[i-1][j], f[i-1][j-1]) + A[i][j]
# 起点f[0][0] = A[0][0]
minTotal = triangle
m = len(triangle)
# n = len(triangle[0])
minTotal[0][0] = triangle[0][0]
# 先处理左边框和右边框
for i in range(1, m):
minTotal[i][0] = minTotal[i-1][0] + triangle[i][0]
minTotal[i][i] = minTotal[i-1][i-1] + triangle[i][i]
for i in range(1, m):
for j in range(1, i):
# if j != 0 and i != j:
minTotal[i][j] = min(minTotal[i-1][j-1], minTotal[i-1][j]) + triangle[i][j]
return min(minTotal[m-1])
二刷
如果不会动态规划呢?下面的解法的目的还是为了加深对动规的理解。分析搜索有什么缺点,突出动规的优点。
遍历,遍历可以想象成一个小人在游走,找到所有可能的路径,再取最小
class Solution:
"""
@param triangle: a list of lists of integers
@return: An integer, minimum path sum
"""
def minimumTotal(self, triangle):
# write your code here
# 这个数字三角形看着和二叉树有点像,尝试用递归的思想做一做
# 遍历
self.n = len(triangle) # 高度为n
self.A = triangle
self.best = float('inf')
self.traverse(0, 0, 0)
return self.best
def traverse(self, x, y, sum):
# 表示从点x, y到底部的路径和
# sum表示到前一层的路径和,不包含x, y这个点
# 递归结束的条件是x == n,n是数字三角形的高度
if x == self.n:
if self.best > sum:
self.best = sum
return self.best
self.traverse(x + 1, y, self.A[x][y] + sum)
self.traverse(x + 1, y + 1, self.A[x][y] + sum)
分治,分而治之,到左边的路径和,到右边的路径和,取最小
class Solution:
"""
@param triangle: a list of lists of integers
@return: An integer, minimum path sum
"""
def minimumTotal(self, triangle):
# write your code here
# 用分治的方法做一做试试
self.n = len(triangle)
self.A = triangle
return self.divideConquer(0, 0)
def divideConquer(self, x, y):
# 表示从x, y位置到底部的最小路径和
# 算出点x, y的左边路径的和,右边路径的和,取最小
# 递归结束的条件是x == n,n是三角形的高
if x == self.n:
return 0
return self.A[x][y] + min(self.divideConquer(x + 1, y), self.divideConquer(x + 1, y + 1))
分治+记忆化搜索,本质上已经是动态规划了。动态规划的思想就是利用以前计算的结果,避免重复计算。
class Solution:
"""
@param triangle: a list of lists of integers
@return: An integer, minimum path sum
"""
def minimumTotal(self, triangle):
# write your code here
# 用分治的方法做一做试试
self.n = len(triangle)
self.A = triangle
# import copy
import sys
# self.hash = copy.deepcopy(triangle) * sys.maxsize
self.hash = {}
for i in range(self.n):
for j in range(i + 1):
self.hash[(i, j)] = sys.maxsize
return self.divideConquer(0, 0)
def divideConquer(self, x, y):
# 表示从x, y位置到底部的最小路径和
# 算出点x, y的左边路径的和,右边路径的和,取最小
# 递归结束的条件是x == n,n是三角形的高
if x == self.n:
return 0
if self.hash[(x, y)] == sys.maxsize:
self.hash[(x, y)] = self.A[x][y] + min(self.divideConquer(x + 1, y), self.divideConquer(x + 1, y + 1))
return self.hash[(x, y)]
动态规划的快是相对与搜索而言的,避免了重复计算。搜索一般是指数级的复杂度,动态规划是多项式级别的复杂度。
三刷,动规思路基本清晰
class Solution:
"""
@param triangle: a list of lists of integers
@return: An integer, minimum path sum
"""
def minimumTotal(self, triangle):
# write your code here
# 也是很典型的坐标型动规
# dp[i][j]表示到[i][j]点的最小路径和
# dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
# 初始化,先初始左右两条边
if len(triangle) == 0 or len(triangle[0]) == 0:
return 0
dp = triangle[:][:]
n = len(triangle)
for i in range(1, n):
dp[i][0] = dp[i - 1][0] + triangle[i][0]
for i in range(1, n):
dp[i][i] = dp[i - 1][i - 1] + triangle[i][i]
for i in range(1, n):
for j in range(1, i):
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j]
return min(dp[n -1])
116. 跳跃游戏
给出一个非负整数数组,你最初定位在数组的第一个位置。数组中的每个元素代表你在那个位置可以跳跃的最大长度。判断你是否能到达数组的最后一个位置。
class Solution:
"""
@param A: A list of integers
@return: A boolean
"""
def canJump(self, A):
# write your code here
# 这个题目视频里讲过,自己也可以理解
# 现在尝试做一做
# f[i]表示是否可以从起始点到达i位置,f[i] = or(f[j], j<i and j+A[j]>=i)
# 起点,f[0] = True
# 终点,f[len(A)-1]
f = [True] + [False] * (len(A) - 1)
# f[0] = True
for i in range(1, len(A)):
for j in range(i):
if f[j] and j + A[j] >= i:
f[i] = True
break
return f[len(A) - 1]
二刷,掌握的不错,可以顺利列出状态转移方程
class Solution:
"""
@param A: A list of integers
@return: A boolean
"""
def canJump(self, A):
# write your code here
# dp[i]表示能否达到位置i
# dp[i] = or(dp[j], j < i and A[j] >= j - i)
if len(A) == 0:
return False
n = len(A)
dp = [False] * n
dp[0] = True
for i in range(1, n):
for j in range(i):
if dp[j] and i - j <= A[j]:
dp[i] = True
return dp[n - 1]
117. 跳跃游戏 II
给出一个非负整数数组,你最初定位在数组的第一个位置。数组中的每个元素代表你在那个位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。
class Solution:
"""
@param A: A list of integers
@return: An integer
"""
def jump(self, A):
# write your code here
# 这个题目在视频里也有,并且是116的follow up
# 自己尝试做一做
# f[i]表示从起始点跳到i位置的最小跳跃次数,f[i] = min(f[j]+1, j<i and j+A[j]>=i)
# 起点f[0] = 0
f = [float('inf')] * len(A) # 先假设最小跳跃次数是无穷大
f[0] = 0
for i in range(1, len(A)):
for j in range(i):
if f[j] != float('inf') and j + A[j] >= i:
f[i] = f[j] + 1
break
return f[len(A) - 1]
# 这个代码不能AC,但是对于学习动规还是很好的。后续二刷的时候有精力再争取AC。
272. 爬楼梯 II
一个小孩爬一个 n 层台阶的楼梯。他可以每次跳 1 步, 2 步 或者 3 步。实现一个方法来统计总共有多少种不同的方式爬到最顶层的台阶。
对于n=0,我们认为答案是1。
class Solution:
"""
@param n: An integer
@return: An Integer
"""
def climbStairs2(self, n):
# write your code here
# 这个应该是坐标型动规吧
# 已经看过了这个类型的视频,应该可以做得出来
# 并且也做过111爬楼梯这个题目了
# 对于最后一层楼梯,小孩可以1步上去,也可以2步上去,或者3步上去
# f[i]表示爬到第i层的方式,f[i] = f[i-1] + f[i-2] + f[i-3]
# 起点f[0] = 1, f[1] = 1, f[2] = 2
if n == 0:
return 1
if n <= 2:
return n
f = [1, 1, 2]
for i in range(3, n + 1):
f.append(f[-1] + f[-2] + f[-3])
return f[-1]
二刷,掌握
76. 最长上升子序列
给定一个整数序列,找到最长上升子序列(LIS),返回LIS的长度。
最长上升子序列问题是在一个无序的给定序列中找到一个尽可能长的由低到高排列的子序列,这种子序列不一定是连续的或者唯一的。
class Solution:
"""
@param nums: An integer array
@return: The length of LIS (longest increasing subsequence)
"""
def longestIncreasingSubsequence(self, nums):
# write your code her
# 这个视频里面讲过的,也属于坐标型动规
# 但是我似乎想不起来具体解法了
# 不要慌,根据已有的动规知识和经验尝试做一做
# 状态方程f[i]表示i位置的最长上升子序列,f[i] = max(f[j]+1, j<i and nums[j]<=nums[i])
# 起点,起点可以从任意位置开始,都初始化为1
# 终点,最大值
if len(nums) == 0:
return 0
f = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(i):
if f[i] <= f[j] and nums[j] < nums[i]:
f[i] = f[j] + 1
return max(f)
二刷,需要三刷
三刷,对动归的思想掌握的比较好
想办法分解问题,列出状态转移方程,进行初始化
class Solution:
"""
@param nums: An integer array
@return: The length of LIS (longest increasing subsequence)
"""
def longestIncreasingSubsequence(self, nums):
# write your code here
# dp[i]表示包含nums[i]的最长上升子序列长度
# dp[i] = max(dp[j], nums[j] < nums[i]) + 1
if len(nums) < 2:
return len(nums)
n = len(nums)
dp = [1] * n
dp[0] = 1
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
107. 单词拆分 I
给定字符串 s 和单词字典 dict,确定 s 是否可以分成一个或多个以空格分隔的子串,并且这些子串都在字典中存在。
因为我们已经使用了更强大的数据,所以普通的DFS方法已经无法通过此题。
class Solution:
"""
@param s: A string
@param wordSet: A dictionary of words dict
@return: A boolean
"""
def wordBreak(self, s, wordSet):
# write your code here
# 九章视频中讲过这个题目,序列型动规,自己尝试做一做
# f[i]表示前i个字符是否可以被拆分
# f[i] = or(f[j], j<i and j+1到i的字符在字典中)
# 起点,f[0] = True,表示前0个字符串即空字符串可以被拆分
# 其余的初始化为False
f = [False] * (len(s) + 1)
f[0] = True
for i in range(1, len(s) + 1):
for j in range(i):
if f[j] == False:
continue
else:
word = s[j:i] # 注意这里索引,为什么不是j+1:i
if word in wordSet:
f[i] = True
return f[-1]
二刷
class Solution:
"""
@param s: A string
@param wordSet: A dictionary of words dict
@return: A boolean
"""
def wordBreak(self, s, wordSet):
# write your code here
# 这是二刷
# 典型的单序列动规问题
# f[i]表示前i个字符是否可以拆分,f[i] = or(f[j], j<i and j+1到i的字符在字典中)
# f[0]表示前0个字符,这是一个空串,初始化为可以拆分,f[0] = True
# f = [False] * (len(s) + 1)
# f[0] = True
# for i in range(1, len(s) + 1):
# for j in range(i):
# if f[j] and s[j:i] in wordSet:
# f[i] = True
# break
# return f[-1]
# 上述解法可以AC,时间复杂度是O(n^2),还有优化的空间
# 因为单词都不长,不可能是非常长的
# s[j:i] in wordSet有很多是没必要判断的
# 可以得到字典中单词的最大长度L,遍历单词的长度
f = [False] * (len(s) + 1)
f[0] = True
wordLength = 0
for ele in wordSet:
if wordLength < len(ele):
wordLength = len(ele)
for i in range(1, len(s) + 1):
if i <= wordLength:
for j in range(i):
if f[j] and s[j:i] in wordSet:
f[i] = True
break
if i > wordLength:
for length in range(1, wordLength + 1):
if f[i - length] and s[i - length:i] in wordSet:
f[i] = True
break
return f[-1]
三刷,值得再刷
class Solution:
"""
@param s: A string
@param wordSet: A dictionary of words dict
@return: A boolean
"""
def wordBreak(self, s, wordSet):
# write your code here
# 是否可以,比较典型的动规题目
# 单序列型动规
# dp[i]表示前i个字符串是否可以被分割
# dp[i] = dp[j], j<i and j-i在字典中
if s in wordSet:
return True
dp = [False] * (len(s) + 1)
dp[0] = True
wordLength = 0
for word in wordSet:
wordLength = max(wordLength, len(word))
for i in range(1, len(s) + 1):
for j in range(i - 1, i - wordLength - 1, -1):
if dp[j] and s[j:i] in wordSet:
dp[i] = True
break
return dp[len(s)]
108. 分割回文串 II
给定字符串 s, 需要将它分割成一些子串, 使得每个子串都是回文串。最少需要分割几次?
class Solution:
"""
@param s: A string
@return: An integer
"""
def minCut(self, s):
# write your code here
# 视频里讲过,序列型动规
# f[i]表示前i个字符最少需要的分割次数,
# f[i] = min(f[j]+1, j<i and j+1到i的字符是回文串)
# 起点f[0] = -1,这个可能不好理解,先记住
# 初始化长度为len(s)+1的数组f
f = [float('inf')] * (len(s) + 1)
f[0] = -1
for i in range(1, len(s) + 1):
for j in range(i):
if f[j] + 1 < f[i] and self.isPalindrome(s[j:i]):
f[i] = f[j] + 1
# break
return f[-1]
def isPalindrome(self, s):
# 判断一个字符串是不是回文串
# 我的第一反应是遍历
res = True
for i in range(len(s) // 2):
if s[i] != s[-1 - i]:
res = False
break
return res
# 时间复杂度有些问题,96%的数据通过。
# 优化我是不能优化了,基本掌握了单序列型动规
# 二刷时优化
二刷
二刷是为了解决时间复杂度的问题,通过解决这个题目,加深了对时间复杂度的理解,可以判断这类双重循环问题的时间复杂度了。
三刷,放上了O(n^2)时间复杂度的代码
class Solution:
"""
@param s: A string
@return: An integer
"""
def minCut(self, s):
# write your code here
# 最少,典型的动规问题,序列型动规
# dp[i]表示前i个字符串最少需要的分割次数
# dp[i] = min(dp[j] + 1, j<i and j-i是回文串)
dp = [i - 1 for i in range(len(s) + 1)]
dp[0] = -1
isPalindrome = [[False] * len(s) for _ in range(len(s))]
for i in range(len(s)):
isPalindrome[i][i] = True
for i in range(len(s) - 1):
isPalindrome[i][i + 1] = (s[i] == s[i + 1])
for length in range(2, len(s)):
for start in range(len(s)):
if start + length >= len(s):
break
if isPalindrome[start + 1][start + length - 1] and s[start] == s[start + length]:
isPalindrome[start][start + length] = True
for i in range(1, len(s) + 1):
for j in range(i):
if isPalindrome[j][i - 1]:
dp[i] = min(dp[i], dp[j] + 1)
return dp[len(s)]
77. 最长公共子序列
给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。
最长公共子序列问题是在一组序列(通常2个)中找到最长公共子序列(注意:不同于子串,LCS不需要是连续的子串)。该问题是典型的计算机科学问题,是文件差异比较程序的基础,在生物信息学中也有所应用。
class Solution:
"""
@param A: A string
@param B: A string
@return: The length of longest common subsequence of A and B
"""
def longestCommonSubsequence(self, A, B):
# write your code here
# 视频里讲过这个题目,双序列动规问题
# 基本方法和单序列类似,也是前i个字符
# 虽然忘记具体解法了,但是刚刚做了两个单序列题目
# 静下心尝试做一做
# f[i][j]表示字符串A的前i个字符配上字符串B的前j的字符的最长公共子序列
# f[i][j] = max(f[i][j-1], f[i-1][j], f[i-1][j-1]) + 1 and A[i] == B[j]
# 起点,初始化,f[i][0] = 0, f[0][j] = 0
# 终点,f[len(A)][len(B)]
f = [[0] * (len(B) + 1) for i in range(len(A) + 1)]
for i in range(1, len(A) + 1):
for j in range(1, len(B) + 1):
f[i][j] = max(f[i - 1][j], f[i][j - 1])
if A[i - 1] == B[j - 1]:
f[i][j] = f[i - 1][j - 1] + 1
return f[len(A)][len(B)]
二刷,需要三刷
三刷,双序列动归,掌握的不好
尤其注意初始化的时候,哪个是行,哪个是列
class Solution:
"""
@param A: A string
@param B: A string
@return: The length of longest common subsequence of A and B
"""
def longestCommonSubsequence(self, A, B):
# write your code here
# dp[i][j]表示A的前i个字符串 B的前j个字符串的最长公共子序列
# dp[i][j] = max(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] + 1 and A[i] == B[j])
dp = [[0] * (len(B) + 1) for _ in range(len(A) + 1)] ## 初始化的时候要注意,原来写的len(A)在前
dp[0][0] = 0
for i in range(1, len(A) + 1):
for j in range(1, len(B) + 1):
if A[i - 1] == B[j - 1]:
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1] + 1)
else:
dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
return dp[-1][-1]
119. 编辑距离
给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。
你总共三种操作方法:
插入一个字符
删除一个字符
替换一个字符
class Solution:
"""
@param word1: A string
@param word2: A string
@return: The minimum number of steps.
"""
def minDistance(self, word1, word2):
# write your code here
# 视频有讲这个题目,但是还木有看
# 也是双序列动规问题,也做了一个最长公共子序列问题了
# 自己先尝试根据双序列动规的讨论做一做这个题目
# f[i][j]表示word1的前i个字符转为word2的前j个字符的最少次数,状态方程见代码吧
# 初始化f[i][0] = i,f[0][j] = j
f = {}
for i in range(len(word1) + 1):
f[(i, 0)] = i
for j in range(len(word2) + 1):
f[(0, j)] = j
for i in range(1, len(word1) + 1):
for j in range(1, len(word2) + 1):
if word1[i - 1] == word2[j-1]:
f[(i, j)] = min(f[(i, j-1)] + 1, f[(i-1, j)] + 1, f[(i-1, j-1)])
else:
f[(i, j)] = min(f[(i, j-1)] + 1, f[(i-1, j)] + 1, f[(i-1, j-1)] + 1)
return f[(len(word1), len(word2))]
# 空间复杂度不达标,二刷时再优化
# 目前主要是掌握动规的思想即典型问题的解法
513. 完美平方
给一个正整数 n, 请问最少多少个完全平方数(比如1, 4, 9…)的和等于n。
class Solution:
"""
@param n: a positive integer
@return: An integer
"""
def numSquares(self, n):
# write your code here
# 应该是序列型动规
# f[i]表示i最少需要的完全平方数,f[i] = min(f[j] + 1, j<i and i-j是完全平方数)
# f[i]初始化为i,f[0] = 0
f = []
for i in range(n + 1):
f.append(i)
for i in range(1, n + 1):
for j in range(i):
if self.isPerfectsquare(i - j):
f[i] = min(f[i], f[j] + 1)
return f[n]
def isPerfectsquare(self, n):
# n为正整数
for i in range(1, n + 1):
if n == i * i:
return True
if n < i * i:
return False
return False
# 以上代码内存使用超过限制。想想也是的,怎么优化呢?
二刷
二刷还是内存超限,不过也有一点点优化
# f[i] = min(f[i - j * j] + 1, and j * j < i) ,这样省掉了完全平方数的判断
def numSquares(self, n):
f = []
for i in range(n + 1): # n最多需要n个完全平方数相加
f.append(i)
for i in range(2, n + 1):
j = 1
while j * j <= i:
f[i] = min(f[i], f[i - j * j] + 1)
j = j + 1
return f[n]
三刷,值得再看看
630. 骑士的最短路径II
在一个 n * m 的棋盘中(二维矩阵中 0 表示空 1 表示有障碍物),骑士的初始位置是 (0, 0) ,他想要达到 (n - 1, m - 1) 这个位置,骑士只能从左边走到右边。找出骑士到目标位置所需要走的最短路径并返回其长度,如果骑士无法达到则返回 -1.
说明
如果骑士所在位置为(x,y),那么他的下一步可以到达以下位置:
(x + 1, y + 2)
(x - 1, y + 2)
(x + 2, y + 1)
(x - 2, y + 1)
class Solution:
"""
@param grid: a chessboard included 0 and 1
@return: the shortest path
"""
def shortestPath2(self, grid):
# write your code here
# 这个问题是从0开始的,比较适合动规
# 611最短路径Ⅰ那个题目,动规是不是不行呢
# f[i][j]表示从起点到达[i][j]需要的最少步数
# f[i][j] = min(f[i+1][j-2], f[i-1][j-2], f[i+2][j-1], f[i-2][j-1]) + 1,点存在且!= 1
# f[0][0] = 0,其它点为无穷大
n = len(grid) # 行
m = len(grid[0]) # 列
f = [[float('inf') for col in range(m)] for row in range(n)]
f[0][0] = 0
for j in range(0, m):
for i in range(0, n):
# 分类讨论
if grid[i][j] != 1 and 0 <= i + 1 < n and 0 <= j - 2 < m and grid[i + 1][j - 2] != 1:
f[i][j] = min(f[i][j], f[i + 1][j - 2] + 1)
if grid[i][j] != 1 and 0 <= i - 1 < n and 0 <= j - 2 < m and grid[i - 1][j - 2] != 1:
f[i][j] = min(f[i][j], f[i - 1][j - 2] + 1)
if grid[i][j] != 1 and 0 <= i + 2 < n and 0 <= j - 1 < m and grid[i + 2][j - 1] != 1:
f[i][j] = min(f[i][j], f[i + 2][j - 1] + 1)
if grid[i][j] != 1 and 0 <= i - 2 < n and 0 <= j - 1 < m and grid[i - 2][j - 1] != 1:
f[i][j] = min(f[i][j], f[i - 2][j - 1] + 1)
return f[n - 1][m -1] if f[n - 1][m -1] != float('inf') else -1
二刷
class Solution:
"""
@param grid: a chessboard included 0 and 1
@return: the shortest path
"""
def shortestPath2(self, grid):
# write your code here
# dp[i][j]表示到[i][j]的最短路径
# dp[i][j] = min(dp[i-1][j-2], dp[i-2][j-1], dp[i+1][j-2], dp[i+2][j-1]) + 1
# 点存在,且点不等于1
n, m = len(grid), len(grid[0])
if n == 0 or m == 0:
return -1
import sys
dp = [[sys.maxsize] * m for _ in range(n)]
dp[0][0] = 0
directions = [(-1, -2), (-2, -1), (1, -2), (2, -1)]
for j in range(m):
for i in range(n):
if grid[i][j] == 1:
continue
for dx, dy in directions:
x, y = i + dx, j + dy
if self.isValid(x, y, grid):
dp[i][j] = min(dp[i][j], dp[x][y] + 1)
if dp[-1][-1] != sys.maxsize:
return dp[-1][-1]
return -1
def isValid(self, x, y, grid):
n, m = len(grid), len(grid[0])
if 0 <= x < n and 0 <= y < m and grid[x][y] != 1:
return True
return False
BFS版本,不理解为什么会超时
class Solution:
"""
@param grid: a chessboard included 0 and 1
@return: the shortest path
"""
def shortestPath2(self, grid):
# write your code here
# dp[i][j]表示到[i][j]的最短路径
# dp[i][j] = min(dp[i-1][j-2], dp[i-2][j-1], dp[i+1][j-2], dp[i+2][j-1]) + 1
# 点存在,且点不等于1
# n, m = len(grid), len(grid[0])
# if n == 0 or m == 0:
# return -1
# import sys
# dp = [[sys.maxsize] * m for _ in range(n)]
# dp[0][0] = 0
# directions = [(-1, -2), (-2, -1), (1, -2), (2, -1)]
# for j in range(m):
# for i in range(n):
# if grid[i][j] == 1:
# continue
# for dx, dy in directions:
# x, y = i + dx, j + dy
# if self.isValid(x, y, grid):
# dp[i][j] = min(dp[i][j], dp[x][y] + 1)
# if dp[-1][-1] != sys.maxsize:
# return dp[-1][-1]
# return -1
# def isValid(self, x, y, grid):
# n, m = len(grid), len(grid[0])
# if 0 <= x < n and 0 <= y < m and grid[x][y] != 1:
# return True
# return False
# 能不能用BFS来做呢,也非常像
n, m = len(grid), len(grid[0])
if n == 0 or m == 0:
return -1
import sys
dp = [[sys.maxsize] * m for _ in range(n)]
dp[0][0] = 0
import queue
q = queue.Queue()
q.put((0, 0))
directions = [(1, 2), (-1, 2), (2, 1), (-2, 1)]
while not q.empty():
head = q.get()
x, y = head[0], head[1]
if grid[x][y] == 1:
continue
for dx, dy in directions:
x_, y_ = x + dx, y + dy
if self.isValid(x_, y_, grid):
dp[x_][y_] = min(dp[x_][y_], dp[x][y] + 1)
q.put((x_, y_))
if dp[-1][-1] != sys.maxsize:
return dp[-1][-1]
return -1
def isValid(self, x, y, grid):
n, m = len(grid), len(grid[0])
if 0 <= x < n and 0 <= y < m and grid[x][y] != 1:
return True
return False
611. 骑士的最短路线
给定骑士在棋盘上的 初始 位置(一个2进制矩阵 0 表示空 1 表示有障碍物),找到到达 终点 的最短路线,返回路线的长度。如果骑士不能到达则返回 -1 。
说明
如果骑士的位置为 (x,y),他下一步可以到达以下这些位置:
(x + 1, y + 2)
(x + 1, y - 2)
(x - 1, y + 2)
(x - 1, y - 2)
(x + 2, y + 1)
(x + 2, y - 1)
(x - 2, y + 1)
(x - 2, y - 1)
注意事项
起点跟终点必定为空.
骑士不能碰到障碍物.
路径长度指骑士走的步数.
"""
Definition for a point.
class Point:
def __init__(self, a=0, b=0):
self.x = a
self.y = b
"""
class Solution:
"""
@param grid: a chessboard included 0 (false) and 1 (true)
@param source: a point
@param destination: a point
@return: the shortest path
"""
# def shortestPath(self, grid, source, destination):
# write your code here
# 现在学习动规,尝试用动规做一做
# 感觉这个问题思路不难,但是实现起来好复杂
# f[i][j]表示起点到[i][j]点的最小步数
# f[i][j] = min(f[i+1][j+2], f[i+1][j-2], f[i+2][j+1], f[i+2][j-1],
# f[i-1][j+2], f[i-1][j-2], f[i-2][j+1], f[i-2][j-1]) + 1, and 这些点都在棋盘上 and != 1
# 初始化起点为0,其它点为无穷大
# m = len(grid) # m行
# n = len(grid[0]) # n列
# f = [[float('inf') for col in range(n)] for row in range(m)]
# f[source.x][source.y] = 0
# for i in range(m):
# for j in range(n):
# # 分8种情况讨论
# if 0 <= i + 1 < n and 0 <= j + 2 < m and grid[i + 1][j + 2] != 1:
# f[i][j] = min(f[i][j], f[i + 1][j + 2] + 1)
# if 0 <= i + 1 < n and 0 <= j - 2 < m and grid[i + 1][j - 2] != 1:
# f[i][j] = min(f[i][j], f[i + 1][j - 2] + 1)
# if 0 <= i + 2 < n and 0 <= j + 1 < m and grid[i + 2][j + 1] != 1:
# f[i][j] = min(f[i][j], f[i + 2][j + 1] + 1)
# if 0 <= i + 2 < n and 0 <= j - 1 < m and grid[i + 2][j - 1] != 1:
# f[i][j] = min(f[i][j], f[i + 2][j - 1] + 1)
# if 0 <= i - 1 < n and 0 <= j + 2 < m and grid[i - 1][j + 2] != 1:
# f[i][j] = min(f[i][j], f[i - 1][j + 2] + 1)
# if 0 <= i - 1 < n and 0 <= j - 2 < m and grid[i - 1][j - 2] != 1:
# f[i][j] = min(f[i][j], f[i - 1][j - 2] + 1)
# if 0 <= i - 2 < n and 0 <= j + 1 < m and grid[i - 2][j + 1] != 1:
# f[i][j] = min(f[i][j], f[i - 2][j + 1] + 1)
# if 0 <= i - 2 < n and 0 <= j - 1 < m and grid[i - 2][j - 1] != 1:
# f[i][j] = min(f[i][j], f[i - 2][j - 1] + 1)
# return f[destination.x][destination.y] if f[destination.x][destination.y] != float('inf') else -1
# 这个题目使用动规是不是不行呢?答案是让用BFS,可以看懂。
# 接下来采用BFS来解决,但是动规的方法还是要思考一下,或者为什么不行?
def shortestPath(self, grid, source, destination):
# BFS是一层一层的来搜索
# f[i][j]表示从起点到这一点的最少步数,初始化为无穷大,但是起点为0
n = len(grid)
m = len(grid[0])
f = [[float('inf') for col in range(m)] for row in range(n)]
f[source.x][source.y] = 0
directions = [(1, 2), (1, -2), (-1, 2), (-1, -2), (2, 1), (2, -1), (-2, 1), (-2, -1)]
import queue
q = queue.Queue(maxsize = n * m)
q.put(source)
while not q.empty():
head = q.get()
for dx, dy in directions:
x = head.x + dx
y = head.y + dy
if 0 <= x < n and 0 <= y < m and grid[x][y] != 1 and f[head.x][head.y] + 1 < f[x][y]:
f[x][y] = f[head.x][head.y] + 1
q.put(Point(x, y))
return f[destination.x][destination.y] if f[destination.x][destination.y] != float('inf') else -1
二刷,可能确实不能动规
力扣 639. 解码方法 II
一条包含字母 A-Z 的消息通过以下的方式进行了编码:
‘A’ -> 1
‘B’ -> 2
…
‘Z’ -> 26
要 解码 一条已编码的消息,所有的数字都必须分组,然后按原来的编码方案反向映射回字母(可能存在多种方式)。例如,“11106” 可以映射为:
“AAJF” 对应分组 (1 1 10 6)
“KJF” 对应分组 (11 10 6)
注意,像 (1 11 06) 这样的分组是无效的,因为 “06” 不可以映射为 ‘F’ ,因为 “6” 与 “06” 不同。
除了 上面描述的数字字母映射方案,编码消息中可能包含 '’ 字符,可以表示从 ‘1’ 到 ‘9’ 的任一数字(不包括 ‘0’)。例如,编码字符串 "1" 可以表示 “11”、“12”、“13”、“14”、“15”、“16”、“17”、“18” 或 “19” 中的任意一条消息。对 “1*” 进行解码,相当于解码该字符串可以表示的任何编码消息。
给你一个字符串 s ,由数字和 ‘*’ 字符组成,返回 解码 该字符串的方法 数目 。
由于答案数目可能非常大,返回对 109 + 7 取余 的结果。
class Solution:
def numDecodings(self, s: str) -> int:
# 采用动规来做,dp[i]表示前i个字符的解码方法个数
# dp[i]可以最后一个字符单独解码,也可以最后两个字符一起解码
# 即dp[i] = alpah * dp[i - 1] + beta * dp[i - 2]
# alpha beta表示最后一个字符的解码方法个数,最后两个字符的一起解码的方法个数
mod = 10 ** 9 + 7
def decoding1digit(ch):
# 最后一个字符可能是星号、0、1-9的数字
if ch == '*':
return 9
if ch == '0':
return 0
return 1
def decoding2digits(ch0, ch1):
# 最后两个字符可能都是星号,可能只有一个是星号,可能都不是星号
if ch0 == ch1 == '*':
return 15
if ch0 == '*':
if ch1 > '6':
return 1
return 2
if ch1 == '*':
if ch0 == '0' or ch0 > '2':
return 0
if ch0 == '1':
return 9
return 6
return 1 if (ch0 != '0' and 1 <= int(ch0) * 10 + int(ch1) <= 26) else 0
# a b c分别表示dp[i - 2] dp[i - 1] dp[i]
a, b, c = 0, 1, 0
for i in range(1, len(s) + 1):
c = b * decoding1digit(s[i - 1])
if i > 1:
c += a * decoding2digits(s[i - 2], s[i - 1])
c = c % mod
a, b = b, c
return c