动态规划
动态规划方法心得
动态规划是一般的面试、笔试中的高频算法题,熟练掌握必要的。动态规划的中心思想是在解决当前问题时,可以由之前已经计算所得的结果并结合现在的限制条件递推出结果。由于此前的计算结果已经保留下来,所以极大的缩短了时间复杂度。
解决动态规划问题的关键是找出状态表达式,即如何由之前的结果推导出现在的结果。另外,有的问题有很多限制条件增加问题的难度,需要剥丝抽茧,将问题解决。在找到状态表达式后,分为三步解决问题:
一. 定义内存空间,用来保存每步结果,并根据题目初始化,有些简单的题目只与前一步相关就不需要定义内存。
二. 根据状态转移表达式依次计算每步的结果,这里需要注意各种限制条件。
三. 将内存中的数据根据要求作为问题的结果返回。
动态规划题目总结
5. 最长回文子串
题目描述: 给定一个字符串 s
,找到 s
中最长的回文子串。你可以假设 s
的最大长度为 1000。
解题思路:动态规划最重要的就是找到状态转移矩阵,此题进行二层循环遍历是否是回文字符串,首先定义一个全为False的二维数组,当前遍历的的结果取决于收尾是否相等和除去收尾的字符串是否为回文字符串两个条件,由此可得状态转移矩阵为:(dp[l, r] = (s[l] == s[r] and (r - l <= 2 or dp[l + 1, r - 1])))。
代码
class Solution:
def longestPalindrome(self, s: str) -> str:
size = len(s)
if size <= 1:
return s
# 二维 dp 问题
# 状态:dp[l,r]: s[l:r] 包括 l,r ,表示的字符串是不是回文串
# 定义二维列表,并规定全部取值为False,这也是后面为什么没有输出False的原因
dp = [[False for _ in range(size)] for _ in range(size)]
longest_l = 1
res = s[0]
# 因为只有 1 个字符的情况在最开始做了判断
# 左边界一定要比右边界小,因此右边界从 1 开始
for r in range(1, size):
for l in range(r):
# 状态转移方程:如果头尾字符相等并且中间也是回文
# 在头尾字符相等的前提下,如果收缩以后不构成区间(最多只有 1 个元素),直接返回 True 即可
# 否则要继续看收缩以后的区间的回文性
# 重点理解 or 的短路性质在这里的作用
if s[l] == s[r] and (r - l <= 2 or dp[l + 1][r - 1]):
dp[l][r] = True
cur_len = r - l + 1
if cur_len > longest_l:
longest_l = cur_len
res = s[l:r + 1]
return res
10. 正则表达式匹配
32. 最长有效括号
44. 通配符匹配
53. 最大子序和
题目描述: 给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
解题思路:思路一:首先,使用第一个数初始化最大连续数组和;然后遍历数组,并依次相加;当超过之前的最大值时就替换,当小于0时就重新初始化为0。思路二:动态规划。遍历数组,状态转移矩阵为:dp[i] = dp[i-1] + dp[i],不过如思路一相同,当前面连续和大于0时就继续加上当前的值,当小于0时就重新计算。此题由于只与前一结果关就没有单独定义列表去储存结果。
思路一:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
max_val, cur = nums[0], 0
for x in nums:
cur += x
if cur > max_val:
max_val = cur
if cur < 0:
cur = 0
return max_val
思路二:
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
max_sum = nums[0]
for i in range(1, n):
if nums[i - 1] > 0:
nums[i] += nums[i - 1]
max_sum = max(nums[i], max_sum)
return max_sum
62. 不同路径
题目描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
解题思路:典型的动态规划题目,首先创建存放结果的列表,然后依次由已知的第一行和第一列循环计算,状态转移矩阵为:dp[i, j] = dp[i-1, j] + dp[i, j-1],最后输出d[-1, -1]。
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# 动态规划
# 定义二维空列表
dp = [[0 for i in range (n+1)] for i in range(m+1)]
# 初始化第一行列表的值
for i in range(1, n+1):
dp[1][i] = 1
# 由状态转移矩阵求解列表其余的值
for i in range(2, m+1):
for j in range(1, n+1):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
# 返回最后的值
return dp[m][n]
63. 不同路径II
题目描述:一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
解题思路:与上题的解题思路相同,不同的是当遇到障碍物时,列表的值不是由状态转移矩阵计算而来,而是直接设为0。
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
# 同样是动态规划,不过在障碍处标记为0,并且不进行遍历;
m, n = len(obstacleGrid), len(obstacleGrid[0])
dp = [[0 for i in range(n+1)] for j in range(m+1)]
for i in range(1, n+1):
if obstacleGrid[0][i-1] == 1:
break
else:
dp[1][i] = 1
for i in range(2,m+1):
for j in range(1,n+1):
if obstacleGrid[i-1][j-1]:
dp[i][j] = 0
else:
dp[i][j]=dp[i-1][j]+dp[i][j-1]
return dp[m][n]
64. 最小路径和
题目描述:给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步
解题思路:典型动态规划题,老套路,定义二维列表,初始化第一行和第一列的值,遍历列表,根据状态转移矩阵dp[i, j] = min(dp[i, j-1], dp[i-1, j]) + grid[i, j]
class Solution(object):
def minPathSum(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
m , n = len(grid) , len(grid[0])
dp = [[0x7fffffff for x in range(n+1)] for x in range(m+1)]
dp[0][1] = dp[1][0] = 0
for i in range(1,m+1):
for j in range(1,n+1):
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1]
return dp[m][n]
70. 爬楼梯
题目描述:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
解题思路:典型动态规划问题,状态转移矩阵:dp[i] =dp[i-1] + dp[i-2]。于此相同的就是斐波那契数列。
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
elif n == 2:
return 2
else:
a, b =1, 2
for i in range(n-2):
c = a + b
a = b
b = c
return c
91. 解码方法
题目描述: 一条包含字母 A-Z
的消息通过以下方式进行了编码:
'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
解题思路:此题和爬楼梯问题类似,不一样的是增加了很多限制条件。(1)最后两位数是否可以解码,也就是是否在1~26之间;(2)只对最后一位进行解码时,最后一位数是否为0;
class Solution:
def numDecodings(self, s: str) -> int:
if not s: return 0
n = len(s)
dp = [0] * n
dp[0] = 1 if s[0] != '0' else 0
for i in range(1, n):
if 10 <= int(s[i - 1:i + 1]) <= 26:
dp[i] += dp[i - 2] if i >= 2 else 1
if s[i] != '0':
dp[i] += dp[i - 1]
return dp[n - 1]
120. 三角形最小路径和
题目描述:给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
解题思路:
说明:如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
mini, M = triangle[-1], len(triangle)
for i in range(M - 2, -1, -1):
for j in range(len(triangle[i])):
mini[j] = triangle[i][j] + min(mini[j], mini[j+1])
return mini[0]