Python数据结构与算法-动态规划(钢条切割问题)

news2024/9/21 16:16:23

一、动态规划(DP)介绍

1、从斐波那契数列看动态规划

(1)问题

斐波那契数列递推式:

练习:使用递归和非递归的方法来求解斐波那契数列的第n项

(2)递归方法的代码实现

import time
# 递归求解斐波那契数列
def fibnacci(n):
    if n == 1 or n == 2:
        return 1
    else: # n > 2
        res = fibnacci(n-1) + fibnacci(n-2) # 递推式
        return res

t1 = time.time()   # 开始时间
print(fibnacci(40)) # 运行程序
t2 = time.time() # 结束时间
print(f"递归运行时间:{t2-t1}") # 运行时间

输出结果:

102334155
递归运行时间:30.308536052703857

(3)非递归方法代码实现

import time
# 非递归求解斐波那契数列
def fibnacci_no_rec(n):
    res = [0, 1, 1] # 结果列表,n=0,n=1,n=2时,结果已知
    if n > 2: 
        # 循环递推式,计算结果
        for i in range(n-2): # 例如n=3时,只需要循环一次
            num = res[-1] + res[-2] # 列表的最后一位和列表最后第二位,对应保存的n-1和n-2的值
            res.append(num) # 结果加入到列表中
    return res[n] # 列表第n项,不是res[-1],res有原始3个元素,n<=2时,res[-1]一直是1

t1 = time.time() # 开始时间
print(fibnacci_no_rec(40)) # 运行程序
t2 = time.time() # 结束时间
print(f"非递归运行时间:{t2-t1}")

输出结果:

102334155
非递归运行时间:0.0

(4)动态规划简单理解

1)对比递归方法和非递归方法的运行时间,可以发现同样规模下,斐波那契数列用递归方法计算,运行时间大大超过非递归方法。其主要原因是,递归方法运行是存在子问题的重复运算。即,当计算f(5)时,f(3)将会重复计算2次,f(5)=f(4)+ f(3);f(4)=f(3)+f(2)。

2)非递归方法计算时,每个子问题只计算一次,存在列表中。非递归方法求解斐波那契数列体现了动态规划的思想。

3)动态规划(DP)的思想包含有:

  • 最优子结构,即递推式,只要求解每个子问题的最优解;

  • 重复子问题,必须需要重复计算时,用循环的方式将重复子问题用列表存储起来。

二、钢条切割问题

1、提出问题

某公司出售钢条,出售价格与钢条长度之间的关系如下表:

问题:现有一段长度为n的钢条和上面的价格表,求切割钢条方案,使得总收益最大。

2、问题分析

(1)钢条切割方案举例

长度为4的钢条的所有切割方案如下:(c方案最优)

思考: 长度为n的钢条的不同切割方案有几种?

从例子可以看出,长度为4的钢条一共有8种切割方式。因为长度为4的钢条可以切割3处,每处都有切和不切两种选择,根据排列组合原理得:。则长度为n的钢条的不同切割方案为种。

因此组合方式很多,枚举法求解不合理。

(2)问题求解思路

如上图所示,其中i表示钢条总长度,pi表示整根出售的价格,r[i]表示整个钢条切割后可出售的最高价格,即当前最优解。

1)以i=4时为例,最优解的求解为:

  • 不切割,总价值为9;

  • 切割为1和3,总价值为1+8=9;

  • 切成2和2,总价值为5+5=10,得到最优解。

2)以i=8时为例,最优解的求解为:

  • 不切割,总价值为20;

  • 切成1和7,总价值为19;

  • 切成2和6,总价值为22;

  • 切成3和5,总价值为21;

  • 切成两个4,总价值为20;

对比后,最优解为切成2和6,长度为2的最优解为5,长度为6的最优解为17,其中长度1-7的最优解是如何切割的在假设已经求出,已存储在列表中,因此只需要考虑长度为8时怎么分割价格最高,再回推到长度2和长度6怎么切割价格最高,这就是动态规划的思想,不断求解当前子问题中的最优解。

(3)钢条切割问题-递推式

设长度为n的钢条切割后最优收益值为,可以得出递推式:

参数说明:

  • 第一个参数表示不切割;

  • 其他n-1个参数分别表示另外n-1种不同切割方案,对方案i=1,2,..,n-1:

  • 将钢条切割为长度为i和n-i两段;

  • 方案i的收益为切割两段的最优收益之和。

  • 考虑所有方案,选择其中收益最大的方案。

(4)最优子结构-钢条切割问题

1)可以将求解规模为n的原问题,划分为规模更小的子问题: 完成一次切割后,可以将产生的两段钢条看成两个独立的钢条切个问题。

2)组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。

3)钢条切割满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解。

(5)最优子结构的简化

钢条切割问题还存在更简单的递归求解方法:

  • 从钢条的左边切割下长度为i的一段,只对右边剩下的一段继续进行切割,左边的不再切割

  • 递推式简化为.

  • 不做切割的方案就可以描述为: 左边一段长度为n,收益为Pn,剩余一段长度为0,收益为ro=0。

上述解析:

1)简化后的递推式的计算方式为:

当i=5时,左边不可再切割,右边可继续切割:

  • 切割为1和4,其中1不可再分割,因此总价值为p1+r[4]=1+10=11;

  • 切割为2和3,其中2不可再切割,总价值为p2+r[3]=5+8 =13;

  • 切割为3和2,其中3不可再切割,总价值为p3+r[2]=8+5 =13;

  • 切割为4和1,其中1不可再切割,总价值为p4 + r[1]=9+1=10;

2)原递推式存在的问题:

原递推式也是可以使用的,只不过存在重复计算的情况。

假如,长度为9的钢条,最优切割为(2,2,2,3),那么根据递推式,可以是4和5,5和4,2和7,7和2,在左右都可分割的情况下,得到的结果都是(2,2,2,3),存在子问题多次计算的情况。

3)简化后递推式的优势:

  • 将所有情况都包含,且不重复计算,以i=9为例,最优解为(2,2,2,3)时,只会求解一次2和7,其中7可以分为(2,2,3)。而4和5的话只有5可以分为(2,3),4不可以分割。不存在原递推式的问题。

  • 递推式更简单。

  • 包含所有可能的情况。

三、钢条切割问题:自顶向下实现

1、原递推式代码实现

p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] # 钢条不同长度价格,其索引即为钢条长度

# 未简化前递推式
def cut_rod_recurision_1(p, n):
    # 长度为0,钢条无价值
    if n == 0:
        return 0
    # 递归实现递推式
    else: 
        res = p[n] # 不切割,全局变量
        for i in range(1,n): # 递归的结束条件,循环次数为r1~rn-1
            # 递推式,递归的是不同的切割方法
            res = max(res,cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i)) 
        return res
    
print(cut_rod_recurision_1(p,9))

输出结果:

25

2、简化后递推式实现

p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] # 钢条不同长度价格,其索引即为钢条长度
# 简化后递推式
def cut_rod_recurision_2(p, n): # p是钢条不切割价格,n钢条长度
    # 长度为0,钢条无价值
    if n == 0:
        return 0
    # 简化后递推式
    else:
        res = 0 # 初始结果为0
        for i in range(1,n+1): # 从p1+rn-1到pn+r0,因此循环1~n。
            res = max(res, p[i] + cut_rod_recurision_2(p, n-i))  # 递推式
        return res
    
print(cut_rod_recurision_2(p,9))

输出结果:

25

3、对比两种递推式的运行时间

import time

# 时间装饰器
def cal_time(func):
    def wrapper(*args, **kwargs): #函数参数不确定的时候,用*args和**kwargs,前者叫位置参数,后者叫关键字参数。
        t1 = time.time()
        result = func(*args, **kwargs) #运行被装饰的函数
        t2 = time.time()
        print("%s running time: %s secs." % (func.__name__,t2-t1)) #func.__name__装饰器的函数,表示函数名称
        return result
    
    return wrapper

# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] # 钢条不同长度价格,其索引即为钢条长度
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]

# 未简化前递推式
def cut_rod_recurision_1(p, n):
    # 长度为0,钢条无价值
    if n == 0:
        return 0
    # 递归实现递推式
    else: 
        res = p[n] # 不切割,全局变量
        for i in range(1,n): # 递归的结束条件,循环次数为r1~rn-1
            # 递推式,递归的是不同的切割方法
            res = max(res,cut_rod_recurision_1(p, i) + cut_rod_recurision_1(p, n-i)) 
        return res

@cal_time
def c1(p,n): # 递归用语法糖会每层都运行,所以加个外壳
    return cut_rod_recurision_1(p,n)

print(c1(p,15))

# 简化后递推式
def cut_rod_recurision_2(p, n): # p是钢条不切割价格,n钢条长度
    # 长度为0,钢条无价值
    if n == 0:
        return 0
    # 简化后递推式
    else:
        res = 0 # 初始结果为0
        for i in range(1,n+1): # 从p1+rn-1到pn+r0,因此循环1~n。
            res = max(res, p[i] + cut_rod_recurision_2(p, n-i))  # 递推式
        return res

@ cal_time   
def c2(p, n):
    return cut_rod_recurision_2(p, n)
    
print(c2(p,15))

输出结果:

c1 running time: 1.8488576412200928 secs.
42
c2 running time: 0.01401066780090332 secs.
42

输出结果可知,原递推式的运行时间比简化后运行时间长。

原递推式每次都会递归2次,简化后的地递推式每次递归1次。

4、自顶向下递归实现的复杂度

递归求解钢条切割问题即为自顶向下求解,为何实现的效率为这么差?

1)即使是简化后的递推式,递归1次,但仍然存在重复子问题的计算。例如:求解r8时,其中p1+r7,p2+r6,...,p8+r0,r7求解又需要p1+r6,...,p7+r0。此时的r6以及被重复计算了,r5,r4重复计算的次数更多。也就是说在递归的过程中,存在大量的子问题重读计算。

2)如下图所示,求r4需要计算r0,r1,r2,r3;求解r3,需要r2,r1,r0;求解r2需要r1,r0。可以发现r2重复计算2次,r1重复计算4次。

  • 时间复杂度为

四、钢条切割问题:自底向上的实现

1、动态规划解法

观察自顶向下的递归求解,存在重复求解相同的子问题,效率极低,因此提出动态规划的解法。

动态规划的思想:

  • 每个子问题只求解一次,保存求解结果

  • 之后需要此问题时,只需查找保存的结果

2、自底向上代码实现-动态规划

import time

# 时间装饰器
def cal_time(func):
    def wrapper(*args, **kwargs): #函数参数不确定的时候,用*args和**kwargs,前者叫位置参数,后者叫关键字参数。
        t1 = time.time()
        result = func(*args, **kwargs) #运行被装饰的函数
        t2 = time.time()
        print("%s running time: %s secs." % (func.__name__,t2-t1)) #func.__name__装饰器的函数,表示函数名称
        return result
    
    return wrapper

# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] # 钢条不同长度价格,其索引即为钢条长度
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]

@cal_time
def cut_rod_dp(p, n):
    r = [0] # n=0时,r为0
    for i in range(1, n+1): # 需要求解r1~rn,因此循环1~n
        res = 0  # ri的初始值为0
        for j in range(1, i+1): # n=i时,ri = max(p1+ri-1,...,pi+r0),每个ri循环j从1~i
            res = max(res, p[j] + r[i-j])  # 每个子问题求解
        r.append(res) # 每个ri存储到列表中方便取用
    return r[n]  

print(cut_rod_dp(p,20))

结果输出

cut_rod_dp running time: 0.0 secs.
56

3、与递归求解运行时间对比

动态规划求解时间复杂度为,而递归求解最小时间复杂度为,时间复杂度小于递归求解。

如下图所示:求解r4时,需要r3,r2,r1,r0都已经存在列表中,可直接取用,不需要再进行运算,大大减少了计算复杂度。

五、钢条切割问题:重构解

1、重构解问题

(1)问题描述

如何修改动态规划算法,使其不仅输出最优解,还输出最优切割方案?

(2)解题思路

根据简化后的递推式,其中左边部分不再切割,右边部分可以再切割。

对每个子问题,保存切割一次时左边切下的长度,即不再切割的部分,定义为s[i]。

说明:i= 5时,r[5]=13,左边为2,右边为3。此时,左边的2不再分割,保存s[5]=2,而右边的3还可以再分割,转化为i=3的问题。当i=3时,已知s[3]=3,则左边为3,右边为0,结束切割。且长度5的钢条切割方案为[2,3]。

(3)代码实现

# p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] # 钢条不同长度价格,其索引即为钢条长度
p = [0, 1, 5, 8, 9, 10, 17, 17, 20, 21, 23, 24, 26, 27, 27, 28, 30, 33, 36, 39, 40]

def cut_rod_extend(p,n): # 求解ri,si
    r = [0] # ri列表,i=0时,价值为0
    s = [0]  # si列表,i=0时,左侧切割长度为0
    for i in range(1,n+1): # 求解r1~rn的n个解
        res_r = 0  # ri的值,表示最大收益
        res_s = 0  # si的值,价格最大值对应方案左边不切割的长度
        for j in range(1,i+1):  #长度为i的最高价值ri = max(p1+ri-1,...,pi+r0),所以对比1~i个解的大小
            if p[j] + r[i-j] > res_r:  # 对比大小
                res_r = p[j] + r[i-j]
                res_s = j  # 对应的p[j],即左边长度
        r.append(res_r) # ri结果存储至列表
        s.append(res_s) # si结果存储至列表

    return r[n],s # rn的最大价值,s是左边不在切割的长度列表

def cut_rod_solution(p,n): # 求解具体切割方案
    r, s = cut_rod_extend(p,n) # 得到r,s列表
    ans = [] # 切割方案列表
    while n > 0: # 当钢条还有长度时,循环
        ans.append(s[n]) # 将n最大收益时左边不切割的长度存储到方案列表
        n -= s[n] # 钢条右边可继续切割的长度为n-sn
    return ans


r,s = cut_rod_extend(p, 15)    # 最大收益值,及最大收益时左边不再切割部分长度列表
ans = cut_rod_solution(p, 15) # 最优切割方案
print(s)
print(r)
print(ans)

输出结果

[0, 1, 2, 3, 2, 2, 6, 1, 2, 3, 2, 2, 6, 1, 2, 3]
42
[3, 6, 6]

2、动态规划问题关键特征

(1)动态规划方法的应用问题

  • 存在且找到最优子结构,最优化问题

  • 原问题的最优解中涉及多少个子问题

  • 在确定最优解使用哪些子问题时,需要考虑多少种选择

  • 能用递归求解的问题就能用动态规划求解

  • 重叠子问题

  • 递归求解时子问题被重复计算

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/440761.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spark----RDD(弹性分布式数据集)

RDD 文章目录 RDDRDD是什么&#xff1f;为什么需要RDD&#xff1f;RDD的五大属性WordCount中的RDD的五大属性如何创建RDD&#xff1f;RDD的操作两种基本算子/操作/方法/API分区操作重分区操作聚合操作四个有key函数的区别 关联操作排序操作 RDD的缓存/持久化cache和persistchec…

Java学习-MySQL-DQL数据查询-联表查询JOIN

Java学习-MySQL-DQL数据查询-联表查询JOIN 1.分析需求&#xff0c;查找那些字段 2.分析查询的字段来自哪些表 3.确定使用哪种连接查询 4.确定交叉点 5.确定判断条件 操作描述inner join返回左右表的交集left join返回左表&#xff0c;即使右表没有right join返回右表&#xf…

iptables深度总结--基础篇

iptables 五表五链 链&#xff1a;INPUT OUTPUT FORWARD PREROUTING POSTROUTING 表&#xff1a;filter、nat、mangle、raw、security 数据报文刚进网卡&#xff0c;还没有到路由表的时候&#xff0c;先进行了prerouting&#xff0c;进入到路由表&#xff0c;通过目标地址判…

FFMPEG 关于smaple_fmts的理解及ffplay播放PCM

问题 当我将一个aac的音频文件解码为原始的PCM数据后&#xff0c;使用ffplay播放测试是否成功时&#xff0c;需要提供给ffplay 采样率&#xff0c;通道数&#xff0c;PCM的格式类型 3个参数&#xff0c;否则无法播放&#xff01; 所以使用ffprobe 查看原来的aac文件信息&…

Python手写板 画图板 签名工具

程序示例精选 Python手写板 画图板 签名工具 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<Python手写板 画图板 签名工具>>编写代码&#xff0c;代码整洁&#xff0c;规则&am…

别再回答面试官,toFixed采用的是四舍五入啦!

四舍五入大家都知道&#xff0c;但你知道银行家舍入法么&#xff1f;你知道JS里的toFixed实现用的是哪种吗&#xff1f; 前两天我写了篇《0.1 0.2 不等于 0.3&#xff1f;原来是因为这个》&#xff0c;大概就是说&#xff0c;0.1 0.2不等于0.3是因为浮点数精度问题。 结果在…

LinkedList 的特点及优缺点

现在来讲 LinkedList LinkedList 是链表集合&#xff0c;基于链表去存储数据&#xff0c;每一个数据视作一个节点 private static class Node<E> {// 存放的数据E item;// 下一个节点Node<E> next;// 上一个节点Node<E> prev;Node(Node<E> prev, E ele…

【unity实战】2D横版实现人物移动跳跃2——用对象池设计制作冲锋残影的效果(包含源码)

基于上一篇人物移动二段跳进一步优化完善 先看看最终效果 什么是对象池? 在Unity中,对象池是一种重复使用游戏对象的技术。使用对象池的好处是可以减少游戏对象的创建和销毁,从而提高游戏的性能。如果不使用对象池,每次需要创建游戏对象时,都需要调用Unity的Instantiate函…

国内几大技术网站,你最爱和哪个玩耍?

所谓“物以类聚&#xff0c;人以群分” 所谓“士为知己者死&#xff0c;女为悦己者容” 所谓“世上的乌鸦都一般黑&#xff0c;鸽子却各有各的白” CSDN&#xff0c;掘金&#xff0c;博客园等&#xff0c;说起来都是“技术”社区&#xff0c;每个却都有着不同的姿色和用处。至于…

初识Spring——IoC及DI详解

目录 一&#xff0c;什么是Spring Spring设计核心 Spring核心定义 Spring官网 二&#xff0c;什么是IoC IoC思想 控制权的反转 三&#xff0c;什么是DI DI的定义 DI和IoC的关系 一&#xff0c;什么是Spring Spring设计核心 我们常说的Spring其实指的是Spring Framewo…

ABP vNext电商项目落地实战(一)——项目搭建

一、落地条件&#xff1a; 1. .NET5版本 2. DDD 3. ABP vNext 4.ABP CLI &#xff08;ABP的命令行工具&#xff0c;包括ABP的各种模板&#xff09; 5.SQL Server 写在前面&#xff1a;我觉得这个框架的文件分层很凌乱&#xff0c;在企业的实际业务场景中&#xff0c;一般…

vscode+git浅尝

git 安装git以后初始化仓库分支重命名合并分支连接远程仓库推送项目 安装git以后 第一次使用git需要配置用户名和邮箱 任意处打开git终端&#xff0c;譬如鼠标右击点击git bash here 命令分别为&#xff1a; 设置用户名和邮箱 git config --global user.name “username” …

【QA】Python代码调试之解决Segmentation fault (core dumped)问题

Python代码调试之解决Segmentation fault 问题 问题描述排查过程1. 定位错误&#xff0c;2. 解决办法 参考资料 问题描述 Python3执行某一个程序时&#xff0c;报Segmentation fault (core dumped)错&#xff0c;且没有其他任何提示&#xff0c;无法查问题。 Segmentation fa…

jenkins gitlab asp.net core持续集成

什么是jenkins Jenkins直接取自其官方文档&#xff0c;是一个独立的开源自动化服务器&#xff0c;您可以使用它来自动执行与构建、测试、交付或部署软件相关的各种任务。 jenkins可以干什么 Jenkins 通过自动执行某些脚本来生成部署所需的文件来工作。这些脚本称为JenkinsFi…

叶酸聚乙二醇羟基FA-PEG-OH;了解高分子试剂 Folate-PEG-OH

FA-PEG-OH&#xff0c;叶酸-聚乙二醇-羟基 中文名称&#xff1a;叶酸聚乙二醇羟基 英文名称&#xff1a;FA-PEG-OH HO-PEG-FA Folate-PEG-OH 性状&#xff1a;黄色液体或固体&#xff0c;取决于分子量 溶剂&#xff1a;溶于水&#xff0c;DMSO、DMF等常规性有机溶剂 活性基…

【NestJs】使用连接mysql企业级开发规范

本篇将介绍如何建立 NestJs 的数据库连接、并使用数据库联表查询。 简介 Nest 与数据库无关&#xff0c;允许您轻松地与任何 SQL 或 NoSQL 数据库集成。根据您的偏好&#xff0c;您有许多可用的选项。一般来说&#xff0c;将 Nest 连接到数据库只需为数据库加载一个适当的 No…

Delphi DataSnap 流程分析(一)

DataSnap 有三种方式: 1、DataSnap REST Application: Create a DataSnap Server with support for REST Communication and with pages that invoke server methods using Java Script and JSON. 2、DataSnap Server: The DataSnap Server Wizard provides an easy way to i…

怎么把视频中动态的人物P掉,把视频中不要的人物去掉

怎么把视频中动态的人物P掉&#xff1f;很多小伙伴试过ps抠图&#xff0c;但是你试过视频人物抠图吗&#xff1f;其实道理是一样的&#xff0c;但是操作过程却变难了。今天就给大家带来一个简单的方法&#xff0c;轻松去除视频中的人物。不影响整个画面的呈现。 在拍摄旅游视频…

springcloud:快速上手定时任务框架xxl-job(十五)

0. 引言 实际开发中&#xff0c;我们常常遇到需要定时执行的任务&#xff0c;我们可以利用定时线程池或schedule框架等来实现定时任务&#xff0c;但这些方式都有效率、性能上的缺陷&#xff0c;在微服务框架下&#xff0c;我们期望一种更加规整、轻量、可靠的定时任务框架来帮…

【通信接口】UART、IIC、SPI

目录 一、预备知识 1、串行与并行 2、单工与双工 3、波特率 二、UART 三、IIC 四、SPI &#xff08;一对一、一对多&#xff09; 五、IIC、SPI异同点 参考文章&#xff1a;这些单片机接口&#xff0c;一定要熟悉&#xff1a;UART、I2C、SPI、TTL、RS232、RS422、RS485…