Python <算法思想集结>之抽丝剥茧聊动态规划

1. 概述

&#x52A8;&#x6001;&#x89C4;&#x5212;算法应用非常之广泛。

对于算法学习者而言,不跨过 &#x52A8;&#x6001;&#x89C4;&#x5212;这道门,不算真正了解算法。

初接触 &#x52A8;&#x6001;&#x89C4;&#x5212;者,理解其思想精髓会存在一定的难度,本文将通过一个案例,抽丝剥茧般和大家聊聊 &#x52A8;&#x6001;&#x89C4;&#x5212;

动态规划算法有 3 个重要的概念:

  • 重叠子问题。
  • 最优子结构。
  • 状态转移。

只有吃透这 3 个概念,才叫真正理解什么是 &#x52A8;&#x6001;&#x89C4;&#x5212;

什么是重叠子问题?

&#x52A8;&#x6001;&#x89C4;&#x5212;&#x5206;&#x6CBB;&#x7B97;&#x6CD5;有一个相似之处。

将原问题分解成相似的子问题,在求解的过程中通过子问题的解求出原问题的解。

动态规划与分治算法的区别:

  • 分治算法的每一个子问题具有完全独立性,只会被计算一次。

    &#x4E8C;&#x5206;&#x67E5;&#x627E;是典型的 &#x5206;&#x6CBB;&#x7B97;&#x6CD5;实现,其子问题是把数列缩小后再二分查找,每一个子问题只会被计算一次。

  • 动态规划经分解得到的子问题往往不是互相独立的,有些子问题会被重复计算多次,这便是 &#x91CD;&#x53E0;&#x5B50;&#x95EE;&#x9898;
  • 同一个子问题被计算多次,完全是没有必要的,可以缓存已经计算过的子问题,再次需要子问题结果时只需要从缓存中获取便可。这便是动态规划中的典型操作, *优化重叠子问题,通过 &#x7A7A;&#x95F4;&#x6362;&#x65F6;&#x95F4; 的优化手段提高性能。

&#x91CD;&#x53E0;&#x5B50;&#x95EE;&#x9898;并不是动态规划的专利,重叠子问题是一个很普见的现象。

什么最优子结构?

最优子结构是动态规划的必要条件。因为动态规划只能应用于具有 &#x6700;&#x4F18;&#x5B50;&#x7ED3;&#x6784;的问题,在解决一个原始问题时,是否能套用动态规划算法,分析是否存在最优子结构是关键。

那么!到底什么是最优子结构? 概念其实很简单,局部最优解能决定全局最优解。

如拔河比赛中。如果 A队中的每一名成员的力气都是每一个班上最大的,由他们组成的拔河队毫无疑问,一定是也是所有拔河队中实力最强的。
如果把求解哪一个团队的力量最大当成原始问题,则每一个人的力量是否最大就是子问题,则子问题的最优决定了原始问题的最优。

所以,动态规划多用于求 &#x6700;&#x503C;的应用场景。

不是说有 3 个概念吗!

不急,先把状态转移这个概念放一放,稍后再解释。

2. 流程

下面以一个案例的解决过程描述使用 &#x52A8;&#x6001;&#x89C4;&#x5212;的流程。

问题描述:小兔子的难题。

有一只小兔子站在一片三角形的胡萝卜地的入口,如下图所示,图中的数字表示每一个坑中胡萝卜的数量,小兔子每次只能跳到 &#x5DE6;&#x4E0B;&#x89D2;或者 &#x53F3;&#x4E0B;&#x89D2;的坑中,请问小兔子怎么跳才能得到最多数量的胡萝卜?

首先这个问题是求最值问题, 是否能够使用动态规划求解,则需要一步一步分析,看是否有满足使用动态规划的条件。

Python <算法思想集结>之抽丝剥茧聊动态规划

2.1 是否存在子问题

先来一个分治思想:思考或观察是否能把原始问题分解成相似的子问题,把解决问题的希望寄托在子问题上。

那么,针对上述三角形数列,是否存在子问题?

现在从数字 7出发,兔子有 2 条可行路线。

Python <算法思想集结>之抽丝剥茧聊动态规划

为了便于理解,首先模糊第 3 行后面的数字或假设第 3 行之后根本不存在。

那么原始问题就变成:

  • 先分别求解 &#x8DEF;&#x7EBF; 1&#x8DEF;&#x7EBF; 2上的最大值。 &#x8DEF;&#x7EBF; 1的最大值为 3, &#x8DEF;&#x7EBF; 2上的最大值是 8
  • 然后求解出 &#x8DEF;&#x7EBF; 1&#x8DEF;&#x7EBF; 2两者之间的最大值 8。 把求得的结果和出发点的数字 7 相加, 7+8=15 就是最后答案。

    只有 2 行时,兔子能获得的最多萝卜数为 15,肉眼便能看的出来。

前面是假设第 3 行之后都不存在,现在把第 3 行放开,则路线 1 路线 2的最大值就要发生变化,但是,对于原始问题来讲,可以不用关心路线 1 和路线 2 是怎么获取到最大值,交给子问题自己处理就可以了。

反正,到时从 &#x8DEF;&#x7EBF; 1&#x8DEF;&#x7EBF; 2 的结果中再选择一个最大值就是。

Python <算法思想集结>之抽丝剥茧聊动态规划

把第 3 行放开后, &#x8DEF;&#x7EBF; 1 就要重新更新最大值,如上图所示, &#x8DEF;&#x7EBF; 1也可以分解成子问题,分解后,也只需要关心子问题的返回结果。

  • 路线 1 的子问题有 2个, &#x8DEF;&#x7EBF; 1_1&#x8DEF;&#x7EBF;1_2。求解 2 个子问题的最大值后,再在 2个子问题中选择最大值 8,最后路线 1的最大值为 3+8=11
  • 路线 2 的子问题有 2个, &#x8DEF;&#x7EBF; 2_1&#x8DEF;&#x7EBF;2_2。求解 2 个子问题的最大值后,再在 2个子问题中选择最大值 2,最后路线 2的最大值为 8+2=10

当第 3 行放开后,更新 &#x8DEF;&#x7EBF; 1&#x8DEF;&#x7EBF;2的最大值,对于原始问题而言,它只需要再在 2 个子问题中选择最大值 11,最终问题的解为 7+11=18

如果放开第 4 行,将重演上述的过程。和原始问题一样,都是从一个点出发,求解此点到目标行的最大值。所以说,此问题是存在子问题的。

并且,只要找到子问题的最优解,就能得到最终原始问题的最优解。不仅存在子问题,而且存在最优子结构。

显然,这很符合递归套路:递进给子问题,回溯子问题的结果。

  • 使用 &#x4E8C;&#x7EF4;&#x6570;&#x5217;&#x8868;保存三角形数列中的所有数据。 a=[[7],[3,8],[8,1,2],[2,7,4,4],[4,5,2,6,5]]
  • 原始问题为 f(0&#xFF0C;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
'''

不是说要聊聊动态规划的流程吗!怎么跑到递归上去了。

其实所有能套用 &#x52A8;&#x6001;&#x89C4;&#x5212;的算法题,都可以使用递归实现,因递归平时接触多,从递归切入,可能更容易理解。

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)

执行程序后,久久没有得到结果,甚至会超时。原因何在?如下图:

Python <算法思想集结>之抽丝剥茧聊动态规划

&#x8DEF;&#x7EBF;1_2&#x8DEF;&#x7EBF;2_1的起点都是从同一个地方(蓝色标注的位置)出发。显然,从数字 1(蓝色标注的数字)出发的这条路径会被计算 2 次。在上图中被重复计算的子路径可不止一条。

这便是重叠子问题!子问题被重复计算。

Python <算法思想集结>之抽丝剥茧聊动态规划

当三角形数列的数据不是很多时,重复计算对整个程序的性能的影响微不足道 。如果数据很多时,大量的重复计算会让计算机性能低下,并可能导致最后崩溃。

因为使用递归的时间复杂度为 O(2^n)。当数据的行数变多时,可想而知,性能有多低下。

怎么解决重叠子问题?

答案是:使用缓存,把曾经计算过的子问题结果缓存起来,当再次需要子问题结果时,直接从缓存中获取,就没有必要再次计算。

这里使用字典作为缓存器,以子问题的起始位置为 &#x5173;&#x952E;&#x5B57;,以子问题的结果为 &#x503C;

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)

因使用随机数生成数据,每次运行结果不一样。但是,每次运行后的速度是非常给力的。

当出现重叠子问题时,可以缓存曾经计算过的子问题。

好 !现在到了关键时刻,屏住呼吸,从分析缓存中的数据开始。

使用递归解决问题,从结构上可以看出是 &#x4ECE;&#x4E0A;&#x5411;&#x4E0B;的一种处理机制。所谓 &#x4ECE;&#x4E0A;&#x5411;&#x4E0B;,也就是由原始问题开始一路去寻找答案。从本题来讲,就是从第一行一直找到最后一行,或者说从 &#x672A;&#x77E5;找到`已知

根据递归的特点,可知缓存数据的操作是在回溯过程中发生的。

Python <算法思想集结>之抽丝剥茧聊动态规划

当再次需要调用某一个子问题时,这时才有可能从缓存中获取到已经计算出来的结果。缓存中的数据是每一个子问题的结果,如果知道了某一个子问题,就可以通过子问题计算出父问题。

这时,可能就会有一个想法?

从已知找到未知。

任何一条路径只有到达最后一行后才能知道最后的结果。可以认为,最后一行是已知数据。先缓存最后一行,那么倒数第 2 行每一个位置到最后一行的路径的最大值就可以直接求出来。

同理,知道了倒数第 2 行的每一个位置的路径最大值,就可以求解出倒数第 3行每一个位置上的最大值。以此类推一直到第 1 行。

天呀!多完美,还用什么递归。

可以认为这种思想便是动态规划的核心: 自下向上

2.3 状态转移

还差最后一步,就能把前面的递归转换成动态规划实现。

什么是状态转移?

前面分析从最后 1 开始求最大值过程,是不是有点像田径场上的多人接力赛跑,第 1 名运动力争跑第 1,把状态转移给第 2名运动员,第 2名运动员持续保持第 1,然后把状态转移给第 3运动员,第 3名运动员也保持他这一圈的第 1,一至到最后一名运动员,都保持自己所在那一圈中的第 1。很显然最后结果,他们这个团队一定是第 1名。

把子问题的值传递给另一个子问题,这便是 &#x72B6;&#x6001;&#x8F6C;&#x79FB;。当然在转移过程中,一定会存在一个表达式,用来计算如何转移。

用来保存每一个子问题状态的表称为 dp 表,其实就是前面递归中的缓存器。

用来计算如何转移的表达式,称为状态转移方程式。

Python <算法思想集结>之抽丝剥茧聊动态规划

有了上述的这张表,就可以使用 &#x52A8;&#x6001;&#x89C4;&#x5212;自下向上的方式解决”兔子的难题”这个问题。

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)。

并不是所有的递归操作都可以使用动态规划进行逆操作,只有符合动态规划条件的递归操作才可以。

上述解决问题时,使用了一个 &#x4E8C;&#x7EF4;&#x5217;&#x8868;充当 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.总结

动态规划问题一般都可以使用递归实现,递归是一种自上向下的解决方案,而动态规划是自下向上的解决方案,两者在解决同一个问题时的思考角度不一样,但本质是一样的。

并不是所有的递归操作都能转换成动态规划,是否能使用动态规划算法,则需要原始问题符合 &#x6700;&#x4F18;&#x5B50;&#x7ED3;&#x6784;&#x91CD;&#x53E0;&#x5B50;&#x95EE;&#x9898;2 个条件。在使用动态规划过程中,找到状态转移表达式是关键。

Original: https://www.cnblogs.com/guo-ke/p/16325745.html
Author: 一枚大果壳
Title: Python <算法思想集结>之抽丝剥茧聊动态规划

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/583931/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 初级图论

    2021.12.5:修改例题代码与部分表述,增加基础定义。 2022.4.22:重构文章。 2022.5.21:进行一些增补,添加 Floyd 算法和 SCC 缩点。 2022.5…

    数据结构和算法 2023年6月12日
    068
  • 近期的刷题计划

    最近打算按照一些专题来刷,初步定调为图论、构造题、与dp。但主要还是按照比赛刷题。 Original: https://www.cnblogs.com/johnsonstar/p/…

    数据结构和算法 2023年6月7日
    079
  • CSP-S初赛知识点(持久更新)

    先更新这么多,以后再说吧AK IOI 算法名称 平均复杂度 最好情况 最坏情况 空间复杂度 排序方式 稳定性 冒泡排序 In_place 稳定 选择排序 In_place 不稳定 …

    数据结构和算法 2023年6月7日
    079
  • 订阅号助手APP怎么插入留言板小程序?

    现在很多小伙伴都是都是利用碎片化的时间来编写文章,身边并没有电脑,这个时候就没有办法使用电脑浏览器上的订阅号留言板插件来接入留言功能了,好在我们又开发了手机端在订阅号助手APP上接…

    数据结构和算法 2023年6月16日
    0117
  • 重新认识受控和非受控组件

    作者:霜序 校稿:袋鼠云数栈前端团队运营小组 该文章包含如下内容 受控与非受控组件 非受控组件 受控组件 受控和非受控组件边界 反模式 解决方案 在 HTML 中,表单元素(&lt…

    数据结构和算法 2023年6月12日
    088
  • 错排问题详解

    概念释义 又叫错位排列、重排,即使一个排列所有的元素都不在原来的位置上。 错排问题是组合数学发展史上的一个重要问题,错排数也是一项重要的数。令 ({ a_k } ( 1 \leq …

    数据结构和算法 2023年6月8日
    0115
  • idea在商店无法搜索到插件

    背景:我使用的版本是IDEA ultimate 2019.2 版本印象中,最初安装的时候,商店还是可以用的,突然有一天,就无法使用了。下边直入正题: 解决办法:1、首先浏览器登陆下…

    数据结构和算法 2023年6月8日
    073
  • MySQL常用命令

    MySQL 常用命令大全 mysql 命令用户连接数据库。 mysql 命令格式: mysql -h 主机地址 -u 用户名 -p 用户密码 1) 连接到本机上的 MYSQL 首先…

    数据结构和算法 2023年6月12日
    074
  • CF Edu124 E 题解

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    数据结构和算法 2023年6月12日
    075
  • 14. 构造二叉树

    📃 题目一描述 题目链接:从中序与后序遍历构造二叉树 🔔 解题思路 必须明确条件: 给出一个数组的值中,是没有重复的数字的,即没用节点的数值是相同的! 画图分析:(图来自dong哥…

    数据结构和算法 2023年6月12日
    085
  • 原型模式(创建型)

    原型模式 介绍 定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。 简单理解,就是当需要创建一个指定的对象时,我们刚好有一个这样的对象,但是又…

    数据结构和算法 2023年6月8日
    0134
  • 算法:两个链表的第一个公共节点

    问题 输入两个链表,找出它们的第一个公共节点。 解决 //1、暴力解法 class Solution { ListNode getIntersectionNode(ListNode…

    数据结构和算法 2023年6月12日
    072
  • linux多线程—使用mmap映射实现文件拷贝

    一、代码实现思路 1、示意图 2、示意图注解 循环创建i个线程,将src文件分为i段拷贝到dest文件中 (1)src文件的大小为src_size,前i-1个线程拷贝的文件大小为s…

    数据结构和算法 2023年6月8日
    099
  • 2022杭电多校第四场C-Magic(差分约束)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=7176 代码: 1 #include 2 #include 3 #include 4…

    数据结构和算法 2023年6月8日
    087
  • [NOI2014] 起床困难综合症

    很明显是有关 二进制的的题 and 即 & (按位与) 二进制下同一位同为1,位运算后为1 例如 (1001 & 0111 = 0001) (1001) (&amp…

    数据结构和算法 2023年6月7日
    083
  • G&GH04 本地连接至远程

    平台: Windows 10 学习笔记 转载请注明出处 欢迎留言 本系列文章是 git & github 的入门教程. 本系列文章优势: 如题, 这里我将该资源库命名为 t…

    数据结构和算法 2023年6月12日
    071
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球