1.概念
动态规划不是某一种具体的算法,而是一种算法思想:若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
2.与其他相似算法的区别
(来自leetcode)
(1)分治
解决分治问题的时候,思路就是想办法把问题的规模减小,有时候减小一个,有时候减小一半,然后将每个小问题的解以及当前的情况组合起来得出最终的结果。例如归并排序和快速排序,归并排序将要排序的数组平均地分成两半,快速排序将数组随机地分成两半。然后不断地对它们递归地进行处理。
这里存在有最优的子结构,即原数组的排序结果是在子数组排序的结果上组合出来的,但是不存在重复子问题,因为不断地对待排序的数组进行对半分的时候,两半边的数据并不重叠,分别解决左半边和右半边的两个子问题的时候,没有子问题重复出现,这是动态规划和分治的区别。
(2)贪心
①关于最优子结构
贪心:每一步的最优解一定包含上一步的最优解,上一步之前的最优解无需记录
动态规划:全局最优解中一定包含某个局部最优解,但不一定包含上一步的局部最优解,因此需要记录之前的所有的局部最优解
②关于子问题最优解组合成原问题最优解的组合方式
贪心:如果把所有的子问题看成一棵树的话,贪心从根出发,每次向下遍历最优子树即可,这里的最优是贪心意义上的最优。此时不需要知道一个节点的所有子树情况,于是构不成一棵完整的树
动态规划:动态规划需要对每一个子树求最优解,直至下面的每一个叶子的值,最后得到一棵完整的树,在所有子树都得到最优解后,将他们组合成答案
③结果正确性
贪心不能保证求得的最后解是最佳的,复杂度低
动态规划本质是穷举法,可以保证结果是最佳的,复杂度高
3.类别
(1)线性动态规划
线性动态规划的主要特点是状态的推导是按照问题规模 i 从小到大依次推过去的,较大规模的问题的解依赖较小规模的问题的解。这里问题规模为 i 的含义是考虑前 i 个元素 [0…i] 时问题的解。
状态转移:dp[n] = f(dp[n-1], …, dp[0])
①单串问题:
1)最长上升子序列(LC 300)
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
dp=[]
for i in range(len(nums)):
dp.append(1)
for j in range(i):
if nums[i]>nums[j]:
dp[i]=max(dp[i],dp[j]+1)
return max(dp)
2)最大子序和(LC 53)
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
tmp_sum = 0
res = nums[0]
for num in nums:
tmp_sum = max(tmp_sum + num, num)
res = max(res, tmp_sum)
return res
3)打家劫舍(LC 198)
class Solution:
def rob(self, nums: List[int]) -> int:
dp=[]
if not nums:
return 0
if len(nums)==1:
return nums[0]
if len(nums)==2:
return max(nums)
dp.append(nums[0])
dp.append(max(nums[0:2]))
for i in range(2,len(nums)):
dp.append(max(dp[i-2]+nums[i],dp[i-1]))
return dp[-1]
②双串问题:
③矩阵问题:
1)最小路径和(LC 64)
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
if not grid or not grid[0]:
return 0
r,c=len(grid),len(grid[0])
# dp=[[0]*c]*r
dp= [[0] * c for _ in range(r)]
dp[0][0]=grid[0][0]
for i in range(1,r):
dp[i][0]=dp[i-1][0]+grid[i][0]
for j in range(1,c):
dp[0][j]=dp[0][j-1]+grid[0][j]
for i in range(1,r):
for j in range(1,c):
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]
return dp[r-1][c-1]
2)三角形最小路径和
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
dp=triangle
if not triangle:
return 0
if len(triangle)==1:
return triangle[0][0]
for i in range(1,len(triangle)):
dp[i][0]=dp[i-1][0]+triangle[i][0]
dp[i][-1]=dp[i-1][-1]+triangle[i][-1]
for i in range(2,len(triangle)):
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[len(triangle)-1])
(2)前缀和
(3)区间动态规划
1)最长回文子串(LC 5)
class Solution:
def longestPalindrome(self, s: str) -> str:
n=len(s)
dp=[[False]*n for _ in range(n)]
if n<2:
return s
begin=0
max_len=1
for i in range(n):
dp[i][i]=True
for l in range(2,n+1):
for i in range(n):
if l+i > n:
break
j=l+i-1
if s[i]!=s[j]:
dp[i][j]=False
else:
if l<3:
dp[i][j]=True
else:
dp[i][j]=dp[i+1][j-1]
if dp[i][j] and l>max_len:
begin=i
max_len=l
return s[begin:begin+max_len]