第十一课 基础算法-动态规划思想

python数据结构与算法基础 第十一课

tags:

  • python
  • 路飞学院

categories:

  • python
  • 基础算法
  • 动态规划思想

第一节 动态规划思想

1. 动态规划的介绍

  1. 斐波那契数列: Fn=Fn-1+Fn-2
  2. 练习:使用递归和非递归的方法来求解斐波那契数列的第n项
  3. 动态规划(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. 钢条切割问题的提出

  1. 某公司出售钢条,出售价格与钢条长度之间的关系如下表:
    在这里插入图片描述
  2. 问题:现有一段长度为n的钢条和.上面的价格表,求切割钢条方案,使得总收益最大。
  3. 长度为4的钢条的所有切割方案如下: (c方案最优)
    在这里插入图片描述
  4. 思考:长度为n的钢条的不同切割方案有几种?
    • 2^(n-1)
    • 有n-1个可以切割的地方。可以选择切或者不切。
  5. 最优解如下:
    在这里插入图片描述

2. 钢条切割问题解决方案

  1. 设长度为n的钢条切割后最优收益值为rn,可以得出递推式:
    r n = max ( p n , r 1 + r n 1 , r 2 + r n 2 , , r n 1 + r 1 ) r_{n}=\max \left(p_{n}, r_{1}+r_{n-1}, r_{2}+r_{n-2}, \cdots, r_{n-1}+r_{1}\right)
  2. 第一个参数pn表示不切割
  3. 其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2…n-1
    • 将钢条切割为长度为i和n-i两段
    • 方案i的收益为切割两段的最优收益之和
    • 考察所有的i,选择其中收益最大的方案
  4. 可以将求解规模为n的原问题,划分为规模更小的子问题:完成-次切割后,可以将产生的两段钢条看成两个独立的钢条切个问题。
  5. 组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。
  6. 钢条切割满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解
  7. 钢条切割问题还存在更简单的递归求解方法(可以少一些重复的比较)
  • 从钢条的左边切割下长度为i的一段,只对右边剩下的一段继续进行切割,左边的不再切割 递推式为:
    r n = max 1 i n ( p i + r n i ) r_{n}=\max _{1 \leq i \leq n}\left(p_{i}+r_{n-i}\right)
  • 不做切割的方案就可以描述为:左边一段长度为n,收益为pn,剩余一段长度为0,收益为ro=0。

3. 钢条切割递归代码实现

  1. 使用递归的方法解决问题(自顶向下递归实现):时间复杂度为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. 钢条切割动态规划代码实现

  1. 递归算法由于重复求解相同子问题,效率极低.所以推荐使用递归的算法如下:
  2. 动态规划的思想:
    • 每个子问题只求解一次,保存求解结果
    • 之后需要此问题时,只需查找保存的结果
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. 钢条切割重构解-路径追溯

  1. 如何修改动态规划算法,使其不仅输出最优解,还输出最优切割方案?
  2. 对每个子问题,保存切割一次时左边切下的长度(不需要分割的初始位置,依次递推可得切得位置)
    在这里插入图片描述
# 根据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. 动态规划问题关键特征
    • 最优子结构
    • 原问题的最优解中涉及多少个子问题
    • 在确定最优解使用哪些子问题时,需要考虑多少种选择
  2. 重叠子问题

第三节 动态规划-最长公共子序列问题

1. 最长公共子序列的提出

  1. 一个序列的子序列是在该序列中删去若千元素后得到的序列。
  2. 例:“ABCD’和“BDF”都是“ABCDEFG”的子序列(不用连续, 子串是连续的)
  3. 最长公共子序列(LCS) 问题:给定两个序列X和Y,求X和Y长度最大的公共子
    序列。
    • 例: X=“ABBCBDE” Y=“DBBCDB” LCS(X,Y)=“BBCD”
  4. 应用场景:字符串相似度比, 基因序列对比等

2. 最长公共子序列的解决方案

  1. 空序列是任何序列的子序列
  2. 暴力穷举法的时间复杂度是多少? 2^n
  3. 最长公共子序列是否具有最优子结构性质? 有
    在这里插入图片描述
  4. 例如:要求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"))
发布了61 篇原创文章 · 获赞 8 · 访问量 2830

猜你喜欢

转载自blog.csdn.net/aa18855953229/article/details/103758897