Python <算法思想集结>之抽丝剥茧聊动态规划
1. 概述
动态规划
算法应用非常之广泛。
对于算法学习者而言,不跨过动态规划
这道门,不算真正了解算法。
初接触动态规划
者,理解其思想精髓会存在一定的难度,本文将通过一个案例,抽丝剥茧般和大家聊聊动态规划
。
动态规划算法有 3
个重要的概念:
- 重叠子问题。
- 最优子结构。
- 状态转移。
只有吃透这 3
个概念,才叫真正理解什么是动态规划
。
什么是重叠子问题?
动态规划
和分治算法
有一个相似之处。
将原问题分解成相似的子问题,在求解的过程中通过子问题的解求出原问题的解。
动态规划与分治算法的区别:
分治算法的每一个子问题具有完全独立性,只会被计算一次。
二分查找
是典型的分治算法
实现,其子问题是把数列缩小后再二分查找,每一个子问题只会被计算一次。动态规划经分解得到的子问题往往不是互相独立的,有些子问题会被重复计算多次,这便是
重叠子问题
。同一个子问题被计算多次,完全是没有必要的,可以缓存已经计算过的子问题,再次需要子问题结果时只需要从缓存中获取便可。这便是动态规划中的典型操作,优化重叠子问题,通过
空间换时间
的优化手段提高性能。
重叠子问题
并不是动态规划的专利,重叠子问题是一个很普见的现象。
什么最优子结构?
最优子结构是动态规划的必要条件。因为动态规划只能应用于具有最优子结构
的问题,在解决一个原始问题时,是否能套用动态规划算法,分析是否存在最优子结构是关键。
那么!到底什么是最优子结构?概念其实很简单,局部最优解能决定全局最优解。
如拔河比赛中。如果
A
队中的每一名成员的力气都是每一个班上最大的,由他们组成的拔河队毫无疑问,一定是也是所有拔河队中实力最强的。如果把求解哪一个团队的力量最大当成原始问题,则每一个人的力量是否最大就是子问题,则子问题的最优决定了原始问题的最优。
所以,动态规划多用于求最值
的应用场景。
不是说有 3
个概念吗!
不急,先把状态转移这个概念放一放,稍后再解释。
2. 流程
下面以一个案例的解决过程描述使用动态规划
的流程。
问题描述:小兔子的难题。
有一只小兔子站在一片三角形的胡萝卜地的入口,如下图所示,图中的数字表示每一个坑中胡萝卜的数量,小兔子每次只能跳到左下角
或者右下角
的坑中,请问小兔子怎么跳才能得到最多数量的胡萝卜?
首先这个问题是求最值问题, 是否能够使用动态规划求解,则需要一步一步分析,看是否有满足使用动态规划的条件。
2.1 是否存在子问题
先来一个分治思想:思考或观察是否能把原始问题分解成相似的子问题,把解决问题的希望寄托在子问题上。
那么,针对上述三角形数列,是否存在子问题?
现在从数字7
出发,兔子有 2
条可行路线。
为了便于理解,首先模糊第 3
行后面的数字或假设第 3
行之后根本不存在。
那么原始问题就变成:
先分别求解
路线 1
和路线 2
上的最大值。路线 1
的最大值为3
,路线 2
上的最大值是8
。然后求解出
路线 1
和路线 2
两者之间的最大值8
。 把求得的结果和出发点的数字7
相加,7+8=15
就是最后答案。只有
2
行时,兔子能获得的最多萝卜数为15
,肉眼便能看的出来。
前面是假设第 3
行之后都不存在,现在把第 3
行放开,则路线 1
路线2
的最大值就要发生变化,但是,对于原始问题来讲,可以不用关心路线 1
和路线 2
是怎么获取到最大值,交给子问题自己处理就可以了。
反正,到时从路线 1
和路线 2
的结果中再选择一个最大值就是。
把第 3
行放开后,路线 1
就要重新更新最大值,如上图所示,路线 1
也可以分解成子问题,分解后,也只需要关心子问题的返回结果。
- 路线
1
的子问题有2
个,路线 1_1
和路线1_2
。求解2
个子问题的最大值后,再在2
个子问题中选择最大值8
,最后路线1
的最大值为3+8=11
。 - 路线
2
的子问题有2
个,路线 2_1
和路线2_2
。求解2
个子问题的最大值后,再在2
个子问题中选择最大值2
,最后路线2
的最大值为8+2=10
。
当第 3
行放开后,更新路线 1
和路线2
的最大值,对于原始问题而言,它只需要再在 2
个子问题中选择最大值 11
,最终问题的解为7+11=18
。
如果放开第 4 行,将重演上述的过程。和原始问题一样,都是从一个点出发,求解此点到目标行的最大值。所以说,此问题是存在子问题的。
并且,只要找到子问题的最优解,就能得到最终原始问题的最优解。不仅存在子问题,而且存在最优子结构。
显然,这很符合递归套路:递进给子问题,回溯子问题的结果。
使用
二维数列表
保存三角形数列中的所有数据。a=[[7],[3,8],[8,1,2],[2,7,4,4],[4,5,2,6,5]]
。原始问题为
f(0,0)
从数列的(0,0)
出发,向左下角和右下角前行,一直找到此路径上的数字相加为最大。f(0,0)
表示以第1
行的第1
列数字为起始点。分解原始问题
f(0,0)=a(0,0)+max(f(1,0)+f(1,1))
。因为每一个子问题又可以分解,让表达式更通用
f(i,j)=a(i,j)+max(f(i+1,j)+f(i+1,j+1))
。(i+1,j)
表示(i,j)
的左下角,(i+1,j+1)
表示(i,j)
的右下角,
编码实现:
# 已经数列
nums = [[7], [3, 8], [8, 1, 2], [2, 7, 4, 4], [4, 5, 2, 6, 5]]
# 递归函数
def get_max_lb(i, j):
if i == len(nums) - 1:
# 递归出口
return nums[i][j]
# 分解子问题
return nums[i][j] + max(get_max_lb(i + 1, j), get_max_lb(i + 1, j + 1))
# 测试
res = get_max_lb(0, 0)
print(res)
'''
输出结果
30
'''
不是说要聊聊动态规划的流程吗!怎么跑到递归上去了。
其实所有能套用动态规划
的算法题,都可以使用递归实现,因递归平时接触多,从递归切入,可能更容易理解。
2.2 是否存在重叠子问题
先做一个实验,增加三角形数的行数,也就是延长路径线。
import random
nums = []
# 递归函数
def get_max_lb(i, j):
if i == len(nums) - 1:
return nums[i][j]
return nums[i][j] + max(get_max_lb(i + 1, j), get_max_lb(i + 1, j + 1))
# 构建 100 行的二维列表
for i in range(100):
nums.append([])
for j in range(i + 1):
nums[i].append(random.randint(1, 100))
res = get_max_lb(0, 0)
print(res)
执行程序后,久久没有得到结果,甚至会超时。原因何在?如下图:
路线1_2
和路线2_1
的起点都是从同一个地方(蓝色标注的位置)出发。显然,从数字 1
(蓝色标注的数字)出发的这条路径会被计算 2
次。在上图中被重复计算的子路径可不止一条。
这便是重叠子问题!子问题被重复计算。
当三角形数列的数据不是很多时,重复计算对整个程序的性能的影响微不足道 。如果数据很多时,大量的重复计算会让计算机性能低下,并可能导致最后崩溃。
因为使用递归的时间复杂度为O(2^n)
。当数据的行数变多时,可想而知,性能有多低下。
怎么解决重叠子问题?
答案是:使用缓存,把曾经计算过的子问题结果缓存起来,当再次需要子问题结果时,直接从缓存中获取,就没有必要再次计算。
这里使用字典作为缓存器,以子问题的起始位置为关键字
,以子问题的结果为值
。
import random
def get_max_lb(i, j):
if i == len(nums) - 1:
return nums[i][j]
left_max = None
right_max = None
if (i + 1, j) in dic.keys():
# 检查缓存中是否存在子问题的结果
left_max = dic[i + 1, j]
else:
# 缓存中没有,才递归求解
left_max = get_max_lb(i + 1, j)
# 求解后的结果缓存起来
dic[(i + 1, j)] = left_max
if (i + 1, j + 1) in dic.keys():
right_max = dic[i + 1, j + 1]
else:
right_max = get_max_lb(i + 1, j + 1)
dic[(i + 1, j + 1)] = right_max
return nums[i][j] + max(left_max, right_max)
# 已经数列
nums = []
# 缓存器
dic = {}
for i in range(100):
nums.append([])
for j in range(i + 1):
nums[i].append(random.randint(1, 100))
# 递归调用
res = get_max_lb(0, 0)
print(res)
因使用随机数生成数据,每次运行结果不一样。但是,每次运行后的速度是非常给力的。
当出现重叠子问题时,可以缓存曾经计算过的子问题。
好 !现在到了关键时刻,屏住呼吸,从分析缓存中的数据开始。
使用递归解决问题,从结构上可以看出是从上向下
的一种处理机制。所谓从上向下
,也就是由原始问题开始一路去寻找答案。从本题来讲,就是从第一行一直找到最后一行,或者说从未知
找到``已知`。
根据递归的特点,可知缓存数据的操作是在回溯过程中发生的。
当再次需要调用某一个子问题时,这时才有可能从缓存中获取到已经计算出来的结果。缓存中的数据是每一个子问题的结果,如果知道了某一个子问题,就可以通过子问题计算出父问题。
这时,可能就会有一个想法?
从已知找到未知。
任何一条路径只有到达最后一行后才能知道最后的结果。可以认为,最后一行是已知数据。先缓存最后一行,那么倒数第 2
行每一个位置到最后一行的路径的最大值就可以直接求出来。
同理,知道了倒数第 2
行的每一个位置的路径最大值,就可以求解出倒数第 3
行每一个位置上的最大值。以此类推一直到第 1 行。
天呀!多完美,还用什么递归。
可以认为这种思想便是动态规划的核心:自下向上。
2.3 状态转移
还差最后一步,就能把前面的递归转换成动态规划实现。
什么是状态转移?
前面分析从最后 1
开始求最大值过程,是不是有点像田径场上的多人接力赛跑,第 1
名运动力争跑第 1
,把状态转移给第 2
名运动员,第 2
名运动员持续保持第 1
,然后把状态转移给第 3
运动员,第 3
名运动员也保持他这一圈的第 1
,一至到最后一名运动员,都保持自己所在那一圈中的第 1
。很显然最后结果,他们这个团队一定是第 1
名。
把子问题的值传递给另一个子问题,这便是状态转移
。当然在转移过程中,一定会存在一个表达式,用来计算如何转移。
用来保存每一个子问题状态的表称为 dp
表,其实就是前面递归中的缓存器。
用来计算如何转移的表达式,称为状态转移方程式。
有了上述的这张表,就可以使用动态规划
自下向上的方式解决“兔子的难题”这个问题。
nums = [[7], [3, 8], [8, 1, 2], [2, 7, 4, 4], [4, 5, 2, 6, 5]]
# dp列表
dp = []
idx = 0
# 从最后一行开始
for i in range(len(nums) - 1, -1, -1):
dp.append([])
for j in range(len(nums[i])):
if i == len(nums) - 1:
# 最后一行缓存于状态转移表中
dp[idx].append(nums[i][j])
else:
dp[idx].append(nums[i][j] + max(dp[idx - 1][j], dp[idx - 1][j + 1]))
idx += 1
print(dp)
'''
输出结果:
[[4, 5, 2, 6, 5], [7, 12, 10, 10], [20, 13, 12], [23, 21], [30]]
'''
程序运行后,最终输出结果和前面手工绘制的dp
表中的数据一模一样。
其实动态规划实现是前面递归操作的逆过程。时间复杂度是O(n^2)。
并不是所有的递归操作都可以使用动态规划进行逆操作,只有符合动态规划条件的递归操作才可以。
上述解决问题时,使用了一个二维列表
充当dp
表,并保存所有的中间信息。
思考一下,真的有必要保存所有的中间信息吗?
在状态转移过程中,我们仅关心当前得到的状态信息,曾经的状态信息其实完全可以不用保存。
所以,上述程序完全可以使用一个一维列表来存储状态信息。
nums = [[7], [3, 8], [8, 1, 2], [2, 7, 4, 4], [4, 5, 2, 6, 5]]
# dp表
dp = []
# 临时表
tmp = []
# 从最后一行开始
for i in range(len(nums) - 1, -1, -1):
# 把上一步得到的状态数据提出来
tmp = dp.copy()
# 清除 dp 表中原来的数据,准备保存最新的状态数据
dp.clear()
for j in range(len(nums[i])):
if i == len(nums) - 1:
# 最后一行缓存于状态转移表中
dp.append(nums[i][j])
else:
dp.append(nums[i][j] + max(tmp[j], tmp[j + 1]))
print(dp)
'''
输出结果:
[30]
'''
3.总结
动态规划问题一般都可以使用递归实现,递归是一种自上向下的解决方案,而动态规划是自下向上的解决方案,两者在解决同一个问题时的思考角度不一样,但本质是一样的。
并不是所有的递归操作都能转换成动态规划,是否能使用动态规划算法,则需要原始问题符合最优子结构
和重叠子问题
这 2
个条件。在使用动态规划过程中,找到状态转移表达式是关键。
Python <算法思想集结>之抽丝剥茧聊动态规划的更多相关文章
- Python <算法思想集结>之初窥基础算法
1. 前言 数据结构和算法是程序的 2 大基础结构,如果说数据是程序的汽油,算法则就是程序的发动机. 什么是数据结构? 指数据在计算机中的存储方式,数据的存储方式会影响到获取数据的便利性. 现实生活中 ...
- Python算法之动态规划(Dynamic Programming)解析:二维矩阵中的醉汉(魔改版leetcode出界的路径数)
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_168 现在很多互联网企业学聪明了,知道应聘者有目的性的刷Leetcode原题,用来应付算法题面试,所以开始对这些题进行" ...
- Python算法:推导、递归和规约
Python算法:推导.递归和规约 注:本节中我给定下面三个重要词汇的中文翻译分别是:Induction(推导).Recursion(递归)和Reduction(规约) 本节主要介绍算法设计的三个核心 ...
- 『嗨威说』算法设计与分析 - 贪心算法思想小结(HDU 2088 Box of Bricks)
本文索引目录: 一.贪心算法的基本思想以及个人理解 二.汽车加油问题的贪心选择性质 三.一道贪心算法题点拨升华贪心思想 四.结对编程情况 一.贪心算法的基本思想以及个人理解: 1.1 基本概念: 首先 ...
- python算法介绍:希尔排序
python作为一种新的语言,在很多功能自然要比Java要好一些,也容易让人接受,而且不管您是成年人还是少儿都可以学习这个语言,今天就为大家来分享一个python算法教程之希尔排序,现在我们就来看看吧 ...
- GitHub标星2.6万!Python算法新手入门大全
今天推荐一个Python学习的干货. 几个印度小哥,在GitHub上建了一个各种Python算法的新手入门大全,现在标星已经超过2.6万.这个项目主要包括两部分内容:一是各种算法的基本原理讲解,二是各 ...
- 【大爽python算法】递归算法进化之回溯算法(backtracking)
作者自我介绍:大爽歌, b站小UP主 , python1对1辅导老师, 时常直播编程,直播时免费回答简单问题. 前置知识: 递归算法(recursion algorithm). 我的递归教程: [教程 ...
- 安装Python算法库
安装Python算法库 主要包括用NumPy和SciPy来处理数据,用Matplotlib来实现数据可视化.为了适应处理大规模数据的需求,python在此基础上开发了Scikit-Learn机器学习算 ...
- 机器学习&数据挖掘笔记(常见面试之机器学习算法思想简单梳理)
机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) 作者:tornadomeet 出处:http://www.cnblogs.com/tornadomeet 前言: 找工作时( ...
随机推荐
- 界面跳转+Android Studio Button事件的三种方式
今天学习界面跳转 java类总是不能新建成功 看了网上教程 (20条消息) 关于android studio无法创建类或者接口问题的解决方法_qq_39916160的博客-CSDN博客 可以新建了 但 ...
- vConsole移动端调试利器
图示: , 简单的几步操作: 1. 引入cdn 可以从https://www.bootcdn.cn/vConsole/下载,也可以下载保存在本地,直接引用 <!DOCTYPE html ...
- 《头号玩家》AI电影调研报告(五)
4.VR自由行走跑步机 电影中,它可以让玩家朝任意方向无限奔跑并保持在平台最中央,还可以模拟上台阶和走斜坡的情况.而下面这件VR跑步装备,可以让你在游戏世界穿行自如. 奥地利创意公司Cyberith公 ...
- SpringMVC-组件分析之视图解析器(prefix,suffix)
SpringMVC的默认组件都是在DispatcherServlet.properties配置文件中配置的: spring-webmvc->org/springframewrok/web/ser ...
- cat /proc/cpuinfo 讲解
查看cpu信息有什么用呢,我们来看看到底有哪些用处:1.和云服务提供商核算成本,现在基本是cpu和内存的费用最大,硬盘大小几乎被忽略了2.我们写程序时候是会关注多核还是单核的,否则不能充分利用多线程等 ...
- Java学习day13
泛型类格式: 修饰符 class 类名<类型>{ } 常用T.E.K.V等形式的参数表示泛型 使用方式与C++的类模板相似,在创建对象时要明确数据类型 泛型方法定义格式: 修饰符<类 ...
- 1. charles安装配置与抓包详解
Charles简介Charles是一个HTTP代理服务器,HTTP监视器,反转代理服务器,当浏览器连接Charles的代理访问互联网时,Charles可以监控浏览器发送和接收的所有数据.它允许一个开发 ...
- css3 做出顶边倾斜的 梯形 div
效果图: <html> <head> <meta charset="utf-8"> <title>顶边倾斜的div梯形</ti ...
- Vue过渡和动画效果展示(案例、GIF动图演示、附源码)
前言 本篇随笔主要写了Vue过渡和动画基础.多个元素过渡和多个组件过渡,以及列表过渡的动画效果展示.详细案例分析.GIF动图演示.附源码地址获取. 作为自己对Vue过渡和动画效果知识的总结与笔记. 因 ...
- XCTF练习题---MISC---Recover-Deleted-File
XCTF练习题---MISC---Recover-Deleted-File flag:de6838252f95d3b9e803b28df33b4baa 解题步骤: 1.观察题目,下载附件 2. 根据题 ...