一、什么问题能用动态规划
如果一个全局最优的大问题单次决策可以转化为重复执行不同状态子问题最优的多次决策,那么该问题可以用动态规划来解决。
动态规划可以解决递归算法重复执行相同子树的弊端,它是一种用空间复杂度换取时间复杂度的算法。
二、DP常常遇见的4类问题
1、斐波那契数列
1,1,2,3,5.... 第n个数是多少
递推公式:f(n) = f(n - 1) + f(n - 2)
(1)递归过程
def feibonaci(n):
if n == 1 or n == 2:
return 1
return feibonaci(n-1) + feibonaci(n-2)
print(feibonaci(5))
时间复杂度:O(2^n)
(2)DP算法
def Dp(n):
# 初始化状态定义,dp算法一定存在初始化的过程,该题初始化的状态是 n == 1 与 n == 2 的斐波那契的数值
a,b = 1,1
for i in range(2,n):
b,a = a + b,b
return b
print(Dp(10))
时间复杂度:O(n)
2、Maximum Value问题
问题:任意给定一个数组,求它的子数组最大的和是多少,并给出此时的子数组
比如(1,-1,-2,3,5,-1,4,-2)
(1)枚举过程
import sys
def fun(array):
# 使用两个指针进行穷举计算
length = len(array)
max_v = -sys.maxsize # 定义子数组最大值和的值
start,end = 0,0 # 定义和最大子数组前后索引
for i in range(length):
for j in range(i+1,length + 1):
if i + 1 >= length: # 索引越界处理
j = i + 1
result = array[i:j]
else:
result = array[i:j]
result = sum(result)
# 保存最大值
if result > max_v:
start = i # 保存最大值子数组的起始索引
end = j
max_v = result
return max_v,array[start:end]
print(fun((1,-1,-2,3,5,-1,4,-2)))
print(fun([-2, -3, 4, -1, -2, 1, 5, -3]))
时间复杂度:O(n^2)
(2)DP算法
DP设计思考:
定义状态 f(j) 为数组array以第j个数A[j]为结尾的所有子数组的和的最大值
且由于子数组必须具有连贯性,即A[j]必须为结尾数,所以有状态转移方程: f(j) = max(f(j - 1) + A[j], A[j])
由状态转移矩阵可知
tp:
A[j] 为数组array第j个位置的值,索引为 j-1。
import sys
def Dp(array):
max_v = -sys.maxsize # 定义保存的最大值
start,end,l,r = 0,0,0,0 # 定义最大子数组和的索引
length = len(array)
# 定义 0 转态 f(0) 的值为 0
dp = 0
for i in range(length): # 1 .... j 状态进行循环
if dp + array[i] >= array[i]:
dp = dp + array[i]
r = i + 1
else:
dp = array[i]
l = i
# 保存最大子数组的值
if dp > max_v:
max_v = dp # 保存最大值
start = l # 保存最大子数组和的索引
end = r
return max_v,array[start:end]
print(Dp((1,-1,-2,3,5,-1,4,-2)))
print(Dp([-2, -3, 4, -1, -2, 1, 5, -3]))
时间复杂度:O(n)
3、Coin Change 问题 或者 0-1 背包问题
0-1 背包问题与 无限换硬币/青蛙跳台阶问题 区别:
0-1背包问题:这种问题由于每一个子问题只有一个,所以每个子选项决策的时候只有两种状态 “给予”、“不给于”,因此该递归
树的深度是“可计算”的,递归树的深度就是子选项的个数
无限换硬币可以简单理解为升级办的0-1问题,它的子项是无穷多个,因此每一个子状态的决策都要考虑所有的子选项。
(1)0-1 背包问题
给定N种物品【每种一个】和一个背包。物品i的重量是Wi,其价值位Vi ,背包的容量为C。问应该如何选择装入背包的物品,使得转入
背包的物品的总价值为最大??
a. 递归过程
时间复杂度:O(2^n)
b. DP算法
DP思路:
定义状态f[i,j] 为有1,2,3..i个物品,背包容量为j的情况下最大的价值
对于物品 i 有两种状态,放入与不放入,故状态转移方程为:f[i,j] = max(f[i-1,j], f[i-1,j-wi] + vi)
tp:
由 i j 两个索引判断 dp 表为二维结构
import sys
def Dp(w,v,c):
"""
:param w: 物品重量列表
:param v: 物品价值列表
:param c: 背包重量
:return:
"""
w = [0] + list(w) # 定义物品状态 0
v = [0] + list(v) # 定义价值状态 0
# 定义 dp map
col = c + 1
row = len(w)
dp_map = [[-sys.maxsize]*col for i in range(row)]
# 初始化 base
dp_map[0] = [0] * col
for i in range(1,row):
for j in range(col):
if j - w[i] < 0: # 判断包是否可以装下当前状态i的物品
dp_map[i][j] = dp_map[i-1][j]
else:
dp_map[i][j] = max(dp_map[i-1][j],dp_map[i-1][j-w[i]] + v[i])
return dp_map[-1][-1]
w = [2,3,5,5]
v = [2,4,3,7]
c = 10
print(Dp(w,v,c))
时间复杂度: O(mn)
jdx 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |||
C 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |||
idx 0 | V 0 | W 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 2 | 2 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
2 | 4 | 3 | 0 | 0 | 2 | 4 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
3 | 3 | 5 | 0 | 0 | 2 | 4 | 4 | 6 | 6 | 6 | 7 | 7 | 9 |
4 | 7 | 5 | 0 | 0 | 2 | 4 | 4 | 7 | 7 | 9 | 11 | 11 | 13 |
选取的物品有:1,2,4【由状态转移倒推】 |
(2)Coin Change 问题(无穷数量背包问题)
问题:假设有n种无穷数量的硬币,他们的面值为1 <= v1 < v2 < v3 < ... < vn。张三手中有面值为C的纸币,请问如果换取
硬币最少的数量为多少?
a. 递归过程
时间复杂度:O(K^n) K为硬币的种类数,两种硬币就是 2^n
b. DP算法
思考:定义 f[i] 为面值为i的纸币所以换取的最少的硬币数,i为纸币的面值同时也是DP算法转态i。
对于状态i,我可以选择一种面值为vj硬币来换,则剩余面值的最优解为f[i - vj]。
则有状态转移方程:
f[i] = min{
f[i-v1] + 1,
f[i-v2] + 1,
f[i-v3] + 1,
...
f[i-vn] + 1,}
tp:
与 0-1 背包问题不同的是,由于状态转移只有一个,所以DP表只需要一维的。
import sys
def Dp(v,m,c):
"""
:param v: 硬币的面值列表
:param m: 硬币的种类数 m = len(v)
:param c: 纸币的面值
:return: 最少的硬币数
"""
# 定义 DP 表
dp_map = [sys.maxsize for i in range(c+1)]
# 初始化
dp_map[0] = 0
for i in range(1,c+1): # 循环纸币dp状态i
min_v = sys.maxsize #定义保存最少硬币的值
for j in range(m): # 循环硬币的面值
if i >= v[j]: # 必须保证纸币的面额大于当前硬币的面额
if 1 + dp_map[i - v[j]] < min_v:
min_v = 1 + dp_map[i - v[j]]
dp_map[i] = min_v
return dp_map[-1]
arr = [1, 3, 4]
m = len(arr)
n = 6
print(Dp(arr,m,n))
时间复杂度: O(mn)
4、Edit Distance
最小的编辑距离:
A B 两个字符串,将A以 删除、替换、增加 转化为B的最小操作次数
(1)递归过程
pass
(2)DP算法
Dp算法思考:
自定义 f[i,j] 为 长度为i的字符串与长度为j的字符串最小的编辑距离。A[i]为长度为i的字符串最后一个字符,B[j]同理
由 删除、增加、替换三种操作方式可以获得状态转移方程:
if A[i] == b[j]:
f[i,j] = f[i-1,j-1]
else:
f[i,j] = min{
f[i,j] = f[i-1,j] + 1, # A 字符串进行删除操作
f[i,j] = f[i,j-1] + 1, # A 字符串进行增加操作【相当于B字符串删除操作】
f[i,j] = f[i-1,j-1] + 1, # A 字符串进行替换操作
}
import sys
def Dp(A,B):
"""
判断 A B 之间的最小的编辑距离
:param A:
:param B:
:return:
"""
# 定义 DP 表
col = len(B) + 1
row = len(A) + 1
dp_map = [[sys.maxsize]*col for i in range(row)]
# 初始化base
dp_map[0] = list(range(col))
for idx,line in enumerate(dp_map):
line[0] = idx
for i in range(1,row):
for j in range(1,col):
if A[i-1] == B[j-1]: # 分为两种情况:① A B 最后一个字符相同 ② A B 最后一个字符不相同
dp_map[i][j] = dp_map[i-1][j-1]
else:
dp_map[i][j] = min(dp_map[i-1][j] + 1,
dp_map[i][j-1] + 1,
dp_map[i-1][j-1] + 1)
return dp_map[-1][-1]
print (Dp("apple", "applld"))
时间复杂度:O(mn)
‘’ | a | p | p | l | l | d | |
‘’ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | 1 | 0 | 1 | 2 | 3 | 4 | 5 |
p | 2 | 1 | 0 | 1 | 2 | 3 | 4 |
p | 3 | 2 | 1 | 0 | 1 | 2 | 3 |
l | 4 | 3 | 2 | 1 | 0 | 1 | 2 |
e | 5 | 4 | 3 | 2 | 1 | 1 | 2 |
5、最长公共子序列LCS与最长公共子串,并返回这些子序列或子串
理解最长公共子序列LCS与最长公共子串的区别:最长公共子序列和最长公共子串(python),这是一种与最短编辑距离类似的问题
(1)最长公共子序列LCS并返回这些子序列
DP算法思考:
自定义 f[i,j] 为 长度为i的字符串与长度为j的字符串最大公共子序列的长度。A[i]为长度为i的字符串最后一个字符,B[j]同理
由 删除、增加 两种操作方式可以获得状态转移方程:
if A[i] == b[j]:
f[i,j] = f[i-1,j-1] + 1
else:
f[i,j] = max{
f[i,j] = f[i-1,j], # A 字符串进行删除操作
f[i,j] = f[i,j-1], # A 字符串进行增加操作【相当于B字符串删除操作】
}
DP算法 【计算出最长的公共子序列长度并且返回其中的一种】
def Dp(A,B):
col = len(B) + 1
row = len(A) + 1
#定义Dp map 并初始化
dp_map = [[0]*col for i in range(row)]
for i in range(1,row):
for j in range(1,col):
if A[i-1] == B[j-1]: # 判断两个字符是否相等
dp_map[i][j] = dp_map[i-1][j-1] + 1
else:
dp_map[i][j] = max(dp_map[i-1][j],dp_map[i][j-1])
# 根据 dp 表反向寻找最长公共子序列【最长的子序列可能有多个,当 dp[i-1][j] = dp[i][j-1] 时,统一向左查询,当然也可以统一向上查询】
i = row - 1 # dp 表行索引
j = col - 1
LCS = '' # 用于保存LCS
while i >= 1 and j >= 1:
if A[i-1] == B[j-1]:
LCS = A[i-1] + LCS
i -= 1
j -= 1
else:
if dp_map[i][j-1] >= dp_map[i-1][j]: # dp表相等的时候统一向左查询
j -= 1
else:
i -= 1
return dp_map[-1][-1],LCS
print(Dp('ABCBDEFBWD','BCDBWD'))
print(Dp('ABC','BC'))
print(Dp("helloworld","loop"))
时间复杂度:O(mn)
(2)最长公共子串并返回这些子串
DP算法思考:
自定义 f[i,j] 为 长度为i的字符串与长度为j的字符串最大公共子串的长度。A[i]为长度为i的字符串最后一个字符,B[j]同理
可以获得状态转移方程:
if A[i] == b[j]:
f[i,j] = f[i-1,j-1] + 1
else:
f[i,j] = 0 # 如果不相等赋值为0
tp:寻找最长的公共子串的最大值不是像公共子序列一样选择f[-1,-1]的值,而是选择dp表中的最值,相同的最值代表有多
个相同的公共子串
DP算法 【计算最长公共子串的长度,并且返回所以有最长公共子串】
import sys
def Dp(A,B):
col = len(B) + 1
row = len(A) + 1
#定义Dp map 并初始化
dp_map = [[0]*col for i in range(row)]
max_v_idxs = [] # 保存最大公共子串的长度最大值的 i j 的值
max_v = -sys.maxsize # 保存最大公共子串的长度的最大值
for i in range(1,row):
for j in range(1,col):
if A[i-1] == B[j-1]: # 判断两个字符是否相等
dp_map[i][j] = dp_map[i-1][j-1] + 1
else:
dp_map[i][j] = 0 # 不相等的话,取0值
# 保存最大值长度数据与索引
if dp_map[i][j] >= max_v: # 以为公共子串可能有多个,所以这里用 >= 号
if dp_map[i][j] > max_v:
max_v_idxs = [] # 遇到新的最大值需要清空以前的数据
max_v = dp_map[i][j]
max_v_idxs.append((i,j)) # 保存公共子串最大值的最后一个字符的i j索引
# 根据索引值去除最长的公共子串【用i 或者是 j 索引都可以截取字符串,这里使用 i 索引,即用 A 字符串进行截取】
max_v_substr = [A[i[0]-max_v:i[0]] for i in max_v_idxs]
return max_v,max_v_substr
print(Dp('ABCBDEFBWD','BCBWD'))
# print(Dp('ABC','BC'))
# print(Dp("helloworld","loop"))
时间复杂度 O(mn)
6、Dynamic Time Wrapping(DTW算法)
问题:DTW最早用于语音识别中,判断两个序列动态时间调整的距离最小值
DTW 问题的特点:
① 两个序列的 首-首 尾-尾 必须相连
② 两个序列点与点互相关联不能与交叉
③ 可以自定义允许‘跳点’。
概念参考:动态时间规整(DTW)算法简介
(1)递归过程
pass
(2)DP算法
DP算法思考:
自定义f[i,j] 为长度为i的数组与长度为j的数组最小的DTW相似距离,dis[i,j] = abs(A[i]-B[j]),指的是A序列在i索引
的值与B序列在j索引的值得差的绝对值。
如果不考虑跳点的情况下,如下图所示,状态传递方向只能是 →、↑、↗
故状态转移方程为:
f[i,j] = min{
f[i-1,j] + dis[i,j],
f[i,j-1] + dis[i,j],
f[i-1,j-1] + dis[i,j],
}
import sys
import numpy as np
def Dp(arr1, arr2):
"""
DIW 算法,本程序适用于无跳点的情况下,即所有的点必须相连
:return:
"""
row = len(arr1)
col = len(arr2)
# 定义 dp map
dp_map = [[sys.maxsize] * col for i in range(row)] # 由于 dtw 两个数组首与首 尾与尾必选相连,这里就以arr1与arr2第一个值的距离作为初始值
# 初始dp
dp_map[0][0] = abs(arr1[0] - arr2[0])
for j in range(1,col):
dp_map[0][j] = dp_map[0][j-1] + abs(arr1[0] - arr2[j])
for i in range(1,row):
dp_map[i][0] = dp_map[i-1][0] + abs(arr1[i] - arr2[0])
# print(np.array(dp_map))
# 开始 dp
for i in range(1,row):
for j in range(1,col):
# DP 过程
dp_map[i][j] = min(
dp_map[i - 1][j] + abs(arr1[i] - arr2[j]),
dp_map[i][j - 1] + abs(arr1[i] - arr2[j]),
dp_map[i - 1][j - 1] + abs(arr1[i] - arr2[j]),
)
# print(np.array(dp_map))
return dp_map[-1][-1]
# arr1 = [1, 3, 7]
# arr2 = [1, 3, 5]
s1 = [1, 2, 3, 4, 5, 5, 5, 4]
s2 = [3, 4, 5, 5, 5, 4]
# s1 = [1, 2]
# s2 = [3, ]
print(Dp(s1, s2))
时间复杂度 O(mn)
- 允许一个“跳点”的DTW算法
状态转移方程:
dp_map[i][j] = min(
dp_map[i - 1][j] + abs(arr1[i] - arr2[j]),
dp_map[i][j - 1] + abs(arr1[i] - arr2[j]),
dp_map[i - 1][j - 1] + abs(arr1[i] - arr2[j]),
dp_map[i - 1][j - 2] + abs(arr1[i] - arr2[j]) if j > 1 else sys.maxsize,
dp_map[i - 2][j - 1] + abs(arr1[i] - arr2[j]) if i > 1 else sys.maxsize,
)
import sys
import numpy as np
def Dp(arr1, arr2):
"""
DIW 算法,本程序适用于允许一个跳点的情况下,最小的DTW距离
:return:
"""
row = len(arr1)
col = len(arr2)
# 定义 dp map
dp_map = [[sys.maxsize] * col for i in range(row)]
# 初始dp
dp_map[0][0] = abs(arr1[0] - arr2[0])
for j in range(1,col):
dp_map[0][j] = dp_map[0][j-1] + abs(arr1[0] - arr2[j])
for i in range(1,row):
dp_map[i][0] = dp_map[i-1][0] + abs(arr1[i] - arr2[0])
# print(np.array(dp_map))
# 开始 dp
for i in range(1,row):
for j in range(max(1,i-10), min(col,i+10)): # 这里面对j的索引进行限制,为了不让过于两个序列索引相差过大的点进行相连
# DP 过程
dp_map[i][j] = min(
dp_map[i - 1][j] + abs(arr1[i] - arr2[j]),
dp_map[i][j - 1] + abs(arr1[i] - arr2[j]),
dp_map[i - 1][j - 1] + abs(arr1[i] - arr2[j]),
dp_map[i - 1][j - 2] + abs(arr1[i] - arr2[j]) if j > 1 else sys.maxsize, # 这里面对 j 索引越界进行判断
dp_map[i - 2][j - 1] + abs(arr1[i] - arr2[j]) if i > 1 else sys.maxsize, # 这里面对 i 索引越界进行判断
)
# print(np.array(dp_map))
return dp_map[-1][-1]
#arr1 = [1, 3, 7]
#arr2 = [1, 3, 5]
s1 = [1, 2, 3, 4, 5, 5, 5, 4]
s2 = [3, 4, 5, 100, 5, 4]
# s1 = [1, 2]
# s2 = [3, ]
print(Dp(s1, s2)) # 最小的 DTW 距离为3 。 100 这个点会被‘跳过’。