Python数据结构与算法-动态规划(钢条切割问题)
一、动态规划(DP)介绍
1、从斐波那契数列看动态规划
(1)问题
斐波那契数列递推式:
练习:使用递归和非递归的方法来求解斐波那契数列的第n项
(2)递归方法的代码实现
import time
# 递归求解斐波那契数列
def fibnacci(n):if n == 1 or n == 2:return 1else: # n > 2res = fibnacci(n-1) + fibnacci(n-2) # 递推式return rest1 = 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]一直是1t1 = 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 resprint(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 # 初始结果为0for 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 resprint(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 resultreturn 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 # 初始结果为0for 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 resultreturn 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为0for i in range(1, n+1): # 需要求解r1~rn,因此循环1~nres = 0 # ri的初始值为0for j in range(1, i+1): # n=i时,ri = max(p1+ri-1,...,pi+r0),每个ri循环j从1~ires = 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,sir = [0] # ri列表,i=0时,价值为0s = [0] # si列表,i=0时,左侧切割长度为0for 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-snreturn ansr,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)动态规划方法的应用问题
存在且找到最优子结构,最优化问题
原问题的最优解中涉及多少个子问题
在确定最优解使用哪些子问题时,需要考虑多少种选择
能用递归求解的问题就能用动态规划求解
重叠子问题
递归求解时子问题被重复计算
(2)动态规划算法的运行过程
动态规划的选择策略是试探性的,每一步要试探所有的可行解并将结果保存起来,最后通过回溯的方法确定最优解,其试探策略称为决策过程
(3)贪心算法与动态规划算法的关系
能用贪心算法解决的问题理论上都可以利用动态规划解决,而一旦证明贪心选择性质,用贪心算法解决问题比动态规划具有更低的时间复杂度和空间复杂度。
Python数据结构与算法-动态规划(钢条切割问题)相关推荐
- python数据结构和算法 时间复杂度分析 乱序单词检测 线性数据结构 栈stack 字符匹配 表达式求值 queue队列 链表 递归 动态规划 排序和搜索 树 图
python数据结构和算法 参考 本文github 计算机科学是解决问题的研究.计算机科学使用抽象作为表示过程和数据的工具.抽象的数据类型允许程序员通过隐藏数据的细节来管理问题领域的复杂性.Pytho ...
- 算法导论 动态规划钢条切割问题 C语言
动态规划钢条切割问题 动态规划(dynamic programming)与分治法类似.分治策略将问题划分为互不相交的子问题,递归求解子问题,再将子问题进行组合,求解原问题.动态规划应用于子问题重叠的情 ...
- 动态规划 — 钢条切割问题
动态规划: 什么是动态规划? 动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息.在求解任一子问题时 ...
- [FreeCodeCamp笔记] Python 数据结构和算法1 二分搜索 Binary Search
我以前学过数据结构和算法(data structure and algorithms. 现在普遍简称DSA),当时用的Robert Sedgewick的coursera课程.这位大神写的<算法( ...
- Python天天美味(32) - python数据结构与算法之堆排序
1. 选择排序 选择排序原理是先选出最小的数,与第一个数交换,然后从第二个数开始再选择最小的数与第二个数交换,-- def selection_sort(data): for i in ran ...
- python数据结构与算法13_python 数据结构与算法 (13)
python 数据结构与算法 (13) 选择排序 (Selection sort) 是? 种简单直观的排序算法. 它的? 作原理如 下.? 先在未排序序列中找到最?(?)元素, 存放到排序序列的起始位 ...
- python leetcode_leetcode 介绍和 python 数据结构与算法学习资料
for (刚入门的编程)的高中 or 大学生 leetcode 介绍 leetcode 可以说是 cs 最核心的一门"课程"了,虽然不是大学开设的,但基本上每一个现代的高水平的程序 ...
- Python数据结构与算法(二)栈和队列
本系列总结了python常用的数据结构和算法,以及一些编程实现. 参考书籍:<数据结构与算法 Python语言实现> [美]Michael T.Goodrich, Roberto Tama ...
- Python数据结构与算法(一)列表和元组
本系列总结了python常用的数据结构和算法,以及一些编程实现. 参考书籍:<数据结构与算法 Python语言实现> [美]Michael T.Goodrich, Roberto Tama ...
最新文章
- sscanf,sscanf_s及其相关用法
- 计算机组成原理——概述
- qt tcp通信_Qt之网络编程TCP通信
- 网站开启https后很慢_网站优化中哪些设置会影响蜘蛛的抓取?对网站SEO产生什么影响...
- 快速迁移Next.js应用到函数计算
- MAC 修改文件夹以及子文件夹和子文件权限 以及 修改文件夹owner
- 电商千万级交易的金手指:分布式事务管理
- Linux Mint---开启桌面三维特效
- 让LYNC安装更容易些
- 计算机网络设备子系统,关于设备间子系统的几点知识学习
- 做微信公众号淘宝客返利系统必须要知道3件事儿
- Word一般文章格式
- 如何使用kettle将EXCEL导入数据库
- 海贝音频384khz_海贝音乐app下载-海贝音乐手机版下载v3.3.0 - 星光下载
- 测试 Windows 8 中的 Metro 风格应用
- 《Excel大神上分攻略》学习笔记1——填充、行列操作、数据格式
- [springboot 开发单体web shop] 1. 前言介绍和环境搭建
- OctetString 转String
- 带你了解软件系统架构的演变
- OA系统的主要功能和作用是什么