动态规划
例子:斐波那契数列:
F
n
=
F
n
−
1
+
F
n
−
2
F_n = F_{n-1} + F_{n-2}
Fn=Fn−1+Fn−2
使用递归和非递归的方法来求解斐波那契数列的第n项。
F
1
=
1
,
F
2
=
1
F_1 = 1, F_2 = 1
F1=1,F2=1
# 子问题的重复计算
# f(5) = f(4) + f(3)
# f(4) = f(3) + f(2)
# f(3) = f(2) + f(1) # f(5)中的f(3)
# f(3) = f(2) + f(1) # f(4)中的f(3)
# 其中 f(3) 被计算了两次,递归就会重复计算
def fibnacci(n):
if n == 1 or n == 2:
return 1
else:
return fibnacci(n-1) + fibnacci(n-2)
# 动态规划(Dynamic Program, DP)思想 = 递推式 + 重复子问题
def fibnacci_no_recurision(n):
f = [0, 1, 1]
if n > 2:
for i in range(n-2):
num = f[-1] + f[-2]
f.append(f)
return f[n]
钢条切割问题
某公司出售钢条,出售价格与钢条长度之间的关系如下表:
问:现有一段长度为n的钢条和上面的价格表,求切割钢条方案,使得总收益最大,则如何切割。
长度为4的钢条的所有切割方案如下:(c方案最优)
思考:长度为n的钢条的不同切割方式有几种?
2
n
2^n
2n,长度为n的钢条有n-1个切割点,则且刚方案有
C
n
−
1
0
+
C
n
−
1
1
+
.
.
.
+
C
n
−
1
n
−
2
+
C
n
−
1
n
−
1
=
2
n
C_{n-1}^0 + C_{n-1}^1 + ... +C_{n-1}^{n-2} + C_{n-1}^{n-1} = 2^n
Cn−10+Cn−11+...+Cn−1n−2+Cn−1n−1=2n, 因此穷举法没办法计算。
下图为长度为n的最优切割价格表,i为长度,r[i]为长度为i的最优价格。
递推式:
设长度为n的钢条,切割后最优收益为
r
n
r_n
rn,可以得出递推式:
-
r
n
=
m
a
x
(
p
n
,
r
1
+
r
n
−
1
,
r
2
+
r
n
−
2
,
.
.
.
,
r
n
−
1
+
r
1
)
r_n = max(p_n, r_1 + r_{n-1}, r_2+r_{n-2},...,r_{n-1}+r_1)
rn=max(pn,r1+rn−1,r2+rn−2,...,rn−1+r1)
第一个参数 p n p_n pn表示不切割
其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2,…,n-1 - 将钢条切割为长度为i和n-i两段
- 方案i的收益为切割两段的最优收益之和
考察所有的i,选择其中收益最大的方案
最优子结构:
可以将求解规模为n的原问题,划分为规模更小的子问题:完成一次切割后可以将产生的两段钢条看成两个独立的钢条切割问题。
组合两个子问题的最优解,并在所以可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。
钢条切割满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解。
钢条切割问题还存在更简单的递归求解方法:
- 从钢条左边切割下单独为i的一端,支队右边剩下的一端继续进行切割,左边不再切割
- 递推式简化为 r n = m a x 1 ≤ i ≤ n ( p i + r n − i ) r_n = \underset{1\le i \le n}{max}(p_i + r_{n-i}) rn=1≤i≤nmax(pi+rn−i)
- 不做切割的方案就可以描述为:左边一段长度为n,收益为 p n p_n pn,剩余一段长度为0,收益为 r 0 = 0 r_0 = 0 r0=0
自顶向下实现
p = [0,1,5,8,9,10,17,17,20,24,30] # p为价格表,其中下标i为钢条长度,对应的p[i] 为i长度钢条的价格
def cut_rod_recurision_1(p, n):
# 原递推式子
if n == 0:
return 0
else:
res = p[n]
for i in range(1, n):
res = max(res, cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i))
return res
def cut_rod_recurision_2(p, n):
# 简化递推式子
if n == 0:
return 0
else:
res = 0
for i in range(1,n+1):
res = max(res, p[i], cut_rod_recurision_2(p, n-i))
return res
动态规划解法
递归算法由于重复求解相同子问题,效率很低
动态规划思想:
- 每个子问题只求解一次,保存求解结果
- 之后需要此问题时,只需要查找保存的结果
自底向上的算法
def cut_rod_dp(p, n):
r = [0]
for i in range(1, n+1):
res = 0
for j in range(1, i+1):
res = max(res, p[j] + r[i-j])
r.append(res)
return r[n]
时间复杂度:O(
n
2
n^2
n2)
输出最优切割方法–重构解
s[i]保存为最优方案中左侧保留的长度(不切的部分)
def cut_rod_extend(p, n):
r = [0]
s = [0]
for i in range(1, n+1):
res_r = 0 # 价格最大值
res_s = 0 # 价格最大值对应方案的左边不切割部分的长度
for j in range(1, i+1):
if p[j] + r[i - j] > res_r:
res_r = p[j] + r[i - j]
res_s = j
r.append(res_r)
s.append(res_s)
return r[n], s
def cut_rod_solution(p, n):
r, s = cut_rod_extend(p, n)
ans = []
while n > 0:
ans.append(s[n])
n -= s[n]
return ans
什么问题可以用动态规划:
1. 最优子结构
- 原问题的最优解中涉及多少个子问题
- 在确定最优解使用哪些子问题时,需要考虑多少种选择
2. 重叠子问题
最长公共子序列
一个序列的子序列是在该序列中删去若干元素后得到的序列
- 例:“ABCD”和“BDF”都是“ABCDEFG”的子序列
最长公共子序列(LCS)问题:给定两个序列X和Y,求X和Y长度最大的公共子序列。
- 例:X = “ABBCBDE” Y = “DBBCDB” LCS(X, Y) = “BBCD”
应用场景:字符串相似度比对
思考:暴力穷举法的时间复杂度?
思考:是否有最优子序列
例如:要求a=“ABCBDAB”, b=“BDCABA”。求这两个的LCS
- 由于最后一位“B”
≠
\neq
=“A”:
- 因此LCS(a,b)应该来源于LCS(a[:-1], b)与 LCS(a,b[:-1])中更大的那一个
def lcs_length(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n+1)] for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if x[i-1] == y[j-1]: # i j位置上的字符匹配的时候,来自于左上方+1
c[i][j] = c[i-1][j-1] + 1
else:
c[i][j] = max(c[i-1][j], c[i][j-1])
return c[m][n]
def lcs(x, y):
m = len(x)
n = len(y)
c = [[0 for _ in range(n+1)] for _ in range(m+1)]
b = [[0 for _ in range(n+1)] for _ in range(m+1)] # 1 左上方, 2 上方, 3 左方
for i in range(1, m+1):
for j in range(1, n+1):
if x[i-1] == y[j-1]: # i j位置上的字符匹配的时候,来自于左上方+1
c[i][j] = c[i-1][j-1] + 1
b[i][j] = 1
elif c[i-1][j] > c[i][j-1]: # 来自于上方
c[i][j] = c[i-1][j]
b[i][j] = 2
else:
c[i][j] = c[i][j-1]
b[i][j] = 3
return c[m][n], b
def lcs_traceback(x, y):
c, b = lcs(x, y)
i = len(x)
j = len(y)
res = []
while i > 0 and j > 0:
if b[i][j] == 1: # 来自左上方
res.append(x[i-1])
i -= 1
j -= 1
elif b[i][j] = 2: # 来自上方
i -= 1
else: # 来自左方
j -= 1
return "".join(reversed(res))