python数据结构与算法基础 第十一课
tags:
- python
- 路飞学院
categories:
- python
- 基础算法
- 动态规划思想
文章目录
第一节 动态规划思想
1. 动态规划的介绍
- 斐波那契数列: Fn=Fn-1+Fn-2
- 练习:使用递归和非递归的方法来求解斐波那契数列的第n项
- 动态规划(DP)
- 最优子结构(实际上就是我们的递推式)
- 重复子问题(避免子问题的重复计算, 可以用循环的方式把子问题存起来。只算一遍)
# 递归写法
# 为什么这么慢呢?子问题的重复计算。时间复杂度为2^n
def fibnacci(n):
if n == 1 or n == 2:
return 1
else:
return fibnacci(n-1) + fibnacci(n-2)
print(fibnacci(10))
# 非递归写法 时间复杂度为n。它把算好的结果保存到f中了
def fibnacci_no_rec(n):
f = [0, 1, 1]
if n > 2:
for i in range(n-2):
num = f[-1] + f[-2]
f.append(num)
return f[n]
print(fibnacci_no_rec(10))
第二节 动态规划-钢条切割问题
1. 钢条切割问题的提出
- 某公司出售钢条,出售价格与钢条长度之间的关系如下表:
- 问题:现有一段长度为n的钢条和.上面的价格表,求切割钢条方案,使得总收益最大。
- 长度为4的钢条的所有切割方案如下: (c方案最优)
- 思考:长度为n的钢条的不同切割方案有几种?
- 2^(n-1)
- 有n-1个可以切割的地方。可以选择切或者不切。
- 最优解如下:
2. 钢条切割问题解决方案
- 设长度为n的钢条切割后最优收益值为rn,可以得出递推式:
- 第一个参数pn表示不切割
- 其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2…n-1
- 将钢条切割为长度为i和n-i两段
- 方案i的收益为切割两段的最优收益之和
- 考察所有的i,选择其中收益最大的方案
- 可以将求解规模为n的原问题,划分为规模更小的子问题:完成-次切割后,可以将产生的两段钢条看成两个独立的钢条切个问题。
- 组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。
- 钢条切割满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解。
- 钢条切割问题还存在更简单的递归求解方法(可以少一些重复的比较)
- 从钢条的左边切割下长度为i的一段,只对右边剩下的一段继续进行切割,左边的不再切割 递推式为:
- 不做切割的方案就可以描述为:左边一段长度为n,收益为pn,剩余一段长度为0,收益为ro=0。
3. 钢条切割递归代码实现
- 使用递归的方法解决问题(自顶向下递归实现):时间复杂度为O(2^n)
import time
def cal_time(func):
def wrapper(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print(func.__name__)
print("%s running time is:%s sesc." % (func.__name__, t2 - t1))
return result
return wrapper
# 钢条切割问题的递归写法一
def cut_rod_recursion1(p, n):
if n == 0:
return 0
else:
res = p[n]
for i in range(1, n):
res = max(res, cut_rod_recursion1(p, i) + cut_rod_recursion1(p, n-i))
return res
# 钢条切割问题的优化后的递归写法
def cut_rod_recursion2(p, n):
if n == 0:
return 0
else:
res = 0
for i in range(1, n+1):
res = max(res, p[i] + cut_rod_recursion2(p, n-i))
return res
# 加装饰器计算时间
@cal_time
def c1(p, n):
return cut_rod_recursion1(p, n)
@cal_time
def c2(p, n):
return cut_rod_recursion2(p, n)
# 显然c2更快一些 指数爆炸
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]
#p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
# c1 running time is:2.3339221477508545 sesc.
# c2 running time is:0.01692795753479004 sesc.
print(c1(p, 15))
print(c2(p, 15))
4. 钢条切割动态规划代码实现
- 递归算法由于重复求解相同子问题,效率极低.所以推荐使用递归的算法如下:
- 动态规划的思想:
- 每个子问题只求解一次,保存求解结果
- 之后需要此问题时,只需查找保存的结果
import time
def cal_time(func):
def wrapper(*args, **kwargs):
t1 = time.time()
result = func(*args, **kwargs)
t2 = time.time()
print(func.__name__)
print("%s running time is:%s sesc." % (func.__name__, t2 - t1))
return result
return wrapper
# 钢条切割问题的动态规划写法
@cal_time
def cut_rod_dp1(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]
# 显然c2更快一些 指数爆炸
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]
#p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
print(cut_rod_dp1(p, 15))
5. 钢条切割重构解-路径追溯
- 如何修改动态规划算法,使其不仅输出最优解,还输出最优切割方案?
- 对每个子问题,保存切割一次时左边切下的长度(不需要分割的初始位置,依次递推可得切得位置)
# 根据s输出我们的解决方案
def cut_rod_solution(p, n):
tmp = n
r, s = cut_rod_extend(p, n)
ans = []
while n > 0:
ans.append(s[n])
n -= s[n]
return "%s段切割方案为:%s,切割后最大价格为%s" % (tmp, ans, r)
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30]
print(cut_rod_solution(p, 9))
6. 动态规划总结
- 动态规划问题关键特征
- 最优子结构
- 原问题的最优解中涉及多少个子问题
- 在确定最优解使用哪些子问题时,需要考虑多少种选择
- 重叠子问题
第三节 动态规划-最长公共子序列问题
1. 最长公共子序列的提出
- 一个序列的子序列是在该序列中删去若千元素后得到的序列。
- 例:“ABCD’和“BDF”都是“ABCDEFG”的子序列(不用连续, 子串是连续的)
- 最长公共子序列(LCS) 问题:给定两个序列X和Y,求X和Y长度最大的公共子
序列。- 例: X=“ABBCBDE” Y=“DBBCDB” LCS(X,Y)=“BBCD”
- 应用场景:字符串相似度比, 基因序列对比等
2. 最长公共子序列的解决方案
- 空序列是任何序列的子序列
- 暴力穷举法的时间复杂度是多少? 2^n
- 最长公共子序列是否具有最优子结构性质? 有
- 例如:要求a="ABCBDAB"与b="BDCABA"的LCS:
- 由于最后一位"B"≠"A":
- 因此LCS(a,b)应该来源于LCS(a[:-1],b)与LCS(a,b[:-1])中更大的那一个
3. 最长公共子序列的代码实现
def lcs_length(x, y):
m = len(x)
n = len(y)
# m+1行 n+1列 因为图中要从0开始(空串是所有序列的子序列)
c = [[0 for _ in range(n+1)] for _ in range(m+1)]
# 从1到m和n
for i in range(1, m+1):
for j in range(1, n+1):
if x[i-1] == y[j-1]:
# 图中斜着传递值情况 最后一个字母匹配来自左上方
c[i][j] = c[i-1][j-1] + 1
else:
c[i][j] = max(c[i][j - 1], c[i - 1][j])
# 调试逐行打印
for _ in c:
print(_)
return c[m][n]
def lcs(x, y):
m = len(x)
n = len(y)
# m+1行 n+1列 因为图中要从0开始(空串是所有序列的子序列)
c = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
# b储存箭头方向 1左上方, 2上方, 3左边 0 没有方向
b = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
# 从1到m和n
for i in range(1, m + 1):
for j in range(1, n + 1):
if x[i - 1] == y[j - 1]:
# 图中斜着传递值情况 最后一个字母匹配来自左上方
c[i][j] = c[i - 1][j - 1] + 1
# 储存左上的箭头
b[i][j] = 1
elif c[i][j - 1] < c[i - 1][j]:
# 从上方来
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 lac_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))
#print(lcs_length("ABCBDAB", "BDCABA"))
print(lac_traceback("ABCBDAB", "BDCABA"))