【算法专题】动态规划的理论与实战

简介: 【算法专题】动态规划的理论与实战

正文


一、为什么要使用动态规划


在前面的文章中,我们介绍了贪心算法、它和动态规划一样,通常都可以用来解决多阶段决策最优解的问题。但是在一些场景下,使用它们的话,并不能解决或者不能很好地解决这种多阶段决策最优解的问题。


我们来看一个例子,假设我们背包能装15公斤重的东西,现在地上总共三种重量的物品若干,分别是1公斤、5公斤和11公斤,那么如何才能使得背包装满15公斤,并且物品数量最少?


如果使用贪心算法的话,每一步都选择当下最优的,那么肯定会选择11+1+1+1+1,那么背包中总共会有5件物品。


如果使用回溯算法的话,就会对1、5、11这三个数字组合成15的排列组合进行穷举,会造成很多肯定不是最优解的垃圾解法,比如15个1公斤重的物品放进背包里面这种肯定不是最优解的解法。这样就会有很高的算法时间复杂度,空耗计算机资源。


正确的解法应该是5+5+5,这个解法才是这个问题的最优解,那么如何找到一个算法来得到这个最优解呢?这就是我们今天要讲的主题——动态规划(Dynamic Programming,DP)。


二、动态规划理论


2.1 动态规划是怎么寻找最优解的?


还是以上面的例子作说明,我们构造一个二维数组,列表示背包中可能会有多少重量的东西,行表示第n次放入物品。


当前我们物品个数没有限制,但是规格只有1、5、11三种,那么在第一次选择物品放入背包的时候,只可能有三种状态,即背包此时重量只可能是1、5、11(见如下表格);


在第二次选择物品放入背包的时候,我们又可以有三种选择,所以应该有九种结果,其中四种超出背包重量限制,所以不符合要求,还有两种重复了,所以此时的合法状态就有四种(见如下表格);


在第三次选择物品放入背包的时候,同理,可以有十二种结果,排除超出背包重量的,和重复的,合法的有5种;此时背包重量首次达到了上限15,所以我们得到了最优解,即+5+5+5这个解法(见如下表格)。


这就是一个动态规划解决问题的思路,把问题分解为多个阶段,每个阶段对应一个决策,我们需要记录每一个阶段的可达状态,当然需要去掉不合法的,合并重复的。然后通过当前这一步得到的状态集合来推导下一个状态集合,如此动态地往前推进,直至遇到最优解。


和回溯算法相比,我们并没有计算所有的情况,那将是指数级增长的时间复杂度;我们把超出背包重量的给排除了,把重复的合并了,把得到最优解后剩下的合法解求解过程给终结了,所以效率上比回溯算法高效很多。



1(放入第一个物品)
2(放入第二个物品) 3(放入第三个物品) 4(放入第四个物品) 5(放入第五个物品)…
0
1 S(+1)
2 S(+1+1)
3 S(+1+1+1)
4
5 S(+5)
6 S(+5+1)(+1+5)
7 S(+5+1+1)(+1+5+1)(+1+1+5)
8
9
10 S(+5+5)
11 S(+11) S(+5+5+1)(+5+1+5)(+1+5+5)
12 S(+11+1)
13 S(+11+1+1)
14
15 S(+5+5+5)


再来一个例子,假设有如下这样一个路径图,要求从左上角通过右移、下移这两个操作来到达右下角的目的地,每个格子中的数字表示需要花费的代价,那么怎么走才能使得代价最小?


664.png


动态规划问题


我们当然可以通过回溯来穷举所有解法,但我们要尝试使用动态规划。首先确定这的确是一个多阶段寻找最优解的问题,而且,划分的步骤应该如下所示。


665.png


动态规划分阶段示意图


同样的,我们构造一个二维数组,其中的数值表示达到这个位置所需要花费的最小代价,即到达这个位置的最优解。然后逐步往终点方向推进,等到了终点时,此时的显示的代价就是我们需要的最优解。


666.png


动态规划求得最优解


如上这种方法被称为状态表转移法。但是要最终写成代码,我们还需要根据上述方法写成状态转移方程,只要有了方程,这个问题就算解决大半了。


我们注意到每个位置上的值都是上面或者左边位置上的值加上本位置上的值的最小值,所以可以得出一个状态转移方程如下:


mind_dist[i][j]=cost[i][j]+min(min_dist[i][j-1],min_dist[i-1][j])

这就有点类似递归了,只不过这里递归得到的都是一个最优子问题的解。当然,在求解每个位置的min_dist值的时候,因为存在重复子问题,所以我们需要将每个子问题的解存下来,供后续需要时直接使用。


2.2 什么样的问题可以考虑使用动态规划来解决呢?


多阶段决策最优解问题模型

判断当前这个问题是否是分为多个阶段来决策,从而寻找出最优解的?

将不同重量的物品放到背包中,就是一个多阶段决策,寻找最优解的问题模型。


最优子结构

当前问题的最优解包含了子问题的最优解。即,我们可以通过子问题的最优解来推导出父问题的最优解。

假设我们最终得到了最优解f(15),那么它的前一个步骤肯定是f(14)、f(10)、f(4)其中的一个,并且只能是它们三者中最优的那个才能最终推导出最优解f(15),所以f(15)肯定包含了其子问题的最优解。


无后效性

在推导父问题解法的状态时候,我们只需要关心其直接子问题的状态值,而不用关心这个子问题的状态值是怎么来的,并且这些子问题状态值不会受到后续解决父问题的影响。

最优解f(15)的推导只关心它的直接子问题f(14)、f(10)、f(4),这些子问题是怎么得出的,和f(15)没有关系,而且并不会因为选择哪一个子问题推导出来f(15),这三个子问题的状态都不会因此而发生变化。基本上只要符合多阶段决策最有问题模型的问题,都能够满足这个特性。


重复子问题

存在重复的子问题;

这个很好理解,f(14)的子问题中我们需要求解f(9),而f(10)的子问题中也存在f(9),这个f(9)就是f(14)和f(10)这两个问题的重复子问题。


三、动态规划使用场景举例


3.1 0-1背包问题


这个问题上面已经有例子了,不再赘述。


3.2 DAG最短路径问题


这个例子在原先讲解贪心算法的时候有提到过。


667.png


求解有向带权图最短路径


使用贪心算法求得的解是S->A->E->T,总代价为1+4+4=9;


而实际最优解是S->B->D->T,总代价为6。


那么如何利用动态规划来求解呢?同样的,我们需要构造一个二维数组,行是需要执行的步骤,列是所有的节点。我们把从起点S开始每一步的合法状态都列举出来,到达每个节点的代价值则用重复子问题中的最小值来代表,这其实就是最优子问题,然后基于每一步动态地往目标推进,最终到达终点T,此时计算出来的最优解就是全局最优解。



step1
step2 step3
S
A 1(S,A)
B 2(S,B)
C 3(S,C)
D 4(S,B,D)(S,C,D)
E 5(S,A,E)(S,C,E)
F 6(S,A,F)(S,B,F)
T 6(S,B,D,T)(S,A,E,T)(S,C,E,T)(S,A,F,T)


3.3 拼写纠错问题


编辑距离,将一个字符串转化成为另一个字符串,需要的最少编辑次数。编辑距离越大,说明相似程度越小,两个相同的字符串其编辑距离为0。

莱文斯坦距离,只允许通过增加、删除、替换字符这三个编辑操作,是衡量两个字符串差异程度的指标。

最长公共子串,只允许通过增加、删除两个编辑操作,是衡量两个字符串相似程度的指标。

以上两种编辑距离的求解过程就可以使用动态规划来解决,可以抽象为把一个字符串变成另一个字符串,求解需要的最少编辑次数。


四、和其它算法的比较


贪心算法、回溯算法和动态规划这三种可以划分为一类,它们都是用来解决多阶段决策最优解问题的解法思路。


其中,回溯算法是一种万能的方法,它通过穷举多阶段的所有路径来找到最优解,缺点就是算法的时间复杂度非常高,只适合小数据量问题的求解,大数据量场景下还是不建议使用它。


贪心算法和动态规划则是两种不同的解决思路,贪心算法比较短视,每个步骤只是选择当下最优的决策,其它决策都抛弃,它是通过每一步的最优来达成最终的最优。贪心算法不是万能的,有些场景下使用它并不能得到最优解。


动态规划则是每个步骤都把合法的状态列出来,动态地向目标推进,这样就不会漏掉全局最优解。但并不是所有问题都可以使用动态规划的,需要满足如上的一个模型三个特征才行。


分治算法单独划分为一类,它不是用来解决多阶段决策最优解问题的,比较适合解决大规模数据求解的问题,它能通过分解问题,合并答案从而达到大数据量问题求解高效的目的。


五、结语


算法专题是一直想写的专题,以后会更多的专注于算法,希望大家多多支持!!!

相关文章
|
8天前
|
大数据 UED 开发者
实战演练:利用Python的Trie树优化搜索算法,性能飙升不是梦!
在数据密集型应用中,高效搜索算法至关重要。Trie树(前缀树/字典树)通过优化字符串处理和搜索效率成为理想选择。本文通过Python实战演示Trie树构建与应用,显著提升搜索性能。Trie树利用公共前缀减少查询时间,支持快速插入、删除和搜索。以下为简单示例代码,展示如何构建及使用Trie树进行搜索与前缀匹配,适用于自动补全、拼写检查等场景,助力提升应用性能与用户体验。
25 2
|
12天前
|
算法 搜索推荐 开发者
别再让复杂度拖你后腿!Python 算法设计与分析实战,教你如何精准评估与优化!
在 Python 编程中,算法的性能至关重要。本文将带您深入了解算法复杂度的概念,包括时间复杂度和空间复杂度。通过具体的例子,如冒泡排序算法 (`O(n^2)` 时间复杂度,`O(1)` 空间复杂度),我们将展示如何评估算法的性能。同时,我们还会介绍如何优化算法,例如使用 Python 的内置函数 `max` 来提高查找最大值的效率,或利用哈希表将查找时间从 `O(n)` 降至 `O(1)`。此外,还将介绍使用 `timeit` 模块等工具来评估算法性能的方法。通过不断实践,您将能更高效地优化 Python 程序。
30 4
|
2月前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点讲解了如何使用 Kotlin 实现 AES-256 的加密和解密,并提供了详细的代码示例。通过生成密钥、加密和解密数据等步骤,展示了如何在 Kotlin 项目中实现数据的安全加密。
59 1
|
2月前
|
机器学习/深度学习 存储 算法
强化学习实战:基于 PyTorch 的环境搭建与算法实现
【8月更文第29天】强化学习是机器学习的一个重要分支,它让智能体通过与环境交互来学习策略,以最大化长期奖励。本文将介绍如何使用PyTorch实现两种经典的强化学习算法——Deep Q-Network (DQN) 和 Actor-Critic Algorithm with Asynchronous Advantage (A3C)。我们将从环境搭建开始,逐步实现算法的核心部分,并给出完整的代码示例。
87 1
|
2月前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点展示了如何使用 Kotlin 实现 AES-256 的加密和解密,提供了详细的代码示例。
39 2
|
2月前
|
机器学习/深度学习 算法 数据挖掘
【白话机器学习】算法理论+实战之决策树
【白话机器学习】算法理论+实战之决策树
|
2月前
|
存储 NoSQL 算法
实战算法篇:设计短域名系统,将长URL转化成短的URL.
小米介绍了一种实用的短域名系统设计,用于将冗长的URL转化为简短链接。短链接不仅节省空间,便于分享,还能支持数据分析。系统通过唯一编号结合62进制转换生成短标识,并利用如Redis这样的数据库存储长链接与短标识的映射关系。最后,通过302重定向实现用户访问时的长链接恢复。这一方案适用于多种场景,有效提升用户体验与数据追踪能力。
63 9
|
2月前
|
算法 搜索推荐 Java
算法实战:手写归并排序,让复杂排序变简单!
归并排序是一种基于“分治法”的经典算法,通过递归分割和合并数组,实现O(n log n)的高效排序。本文将通过Java手写代码,详细讲解归并排序的原理及实现,帮助你快速掌握这一实用算法。
38 0
|
2月前
|
机器学习/深度学习 算法 Java
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
这篇文章介绍了基于贪婪技术思想的Prim算法和Dijkstra算法,包括它们的伪代码描述、Java源代码实现、时间效率分析,并展示了算法的测试用例结果,使读者对贪婪技术及其应用有了更深入的理解。
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
|
2月前
|
算法 Java 测试技术
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
这篇文章介绍了基于动态规划法的三种算法:解决背包问题的递归和自底向上实现、Warshall算法和Floyd算法,并提供了它们的伪代码、Java源代码实现以及时间效率分析。
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
下一篇
无影云桌面