动态规划Dynamic Programming的基础解法

news2024/10/10 6:20:50

本文是对Jeff Erickson经典算法入门书籍《Algorithms》中动态规划问题的阅读笔记,近期在刷一些编程题,对于如何凑出动态规划更新的范式,脑袋里一直是一团乱麻,特别看完了别人的题解,只是惊叹其脑洞,但一直搞不清其是怎么想出来的。《Algorithms》给出了动态规划解决的非常清晰的思路,从递归出发,一步步完成动态规划更新的范式的推理。本文结合个人理解作了总结。

 Jeff指出动态规划本质上是没有重复计算的递归问题,递归算法将原问题拆解为缩小的子问题,而动态规划在于设计合理的递归路径,保存之前已经计算好的子问题的结果,从而减少未来的重复计算。

本文结合了Jeff的步骤,总结了动态规划问题的一般性实现步骤:

  • A) 定义递归形式:将父问题拆分为子问题和问题边界,并由递归来定义原问题F(n) = \sum F(n'), \exists\ n' < n,其中F(n')表示数据量更小的子问题。
  • B) 展开递归路径:确定由子问题到父问题的解决路径。
  • C) 确定合适的执行顺序:保证父问题所有依赖的子问题都在其执行前完成。
  • D) 确定合适的数据结构保存子问题的结果:保存的结果是父问题计算所依赖的,可以采用数组、矩阵、树等等,数据结构的访问一般同问题执行顺序相适配。
  • E) 确定问题边界和问题计算逻辑:根据递归问题的边界条件来初始化数据结构,同时将递归形式转化了动态规划的计算问题。
  • F) 对时间和空间的优化分析:分析空间和时间复杂度,同时分析在执行路径和存储数据结构中是否有冗余。

接下来,我们将举一些例子,展开讨论下通过上述步骤如何解决动态规划问题。

1. 斐波那契Fibonacci问题

A) 定义递归形式:

第n个斐波那契数可以定义为递归的形式,即F(n) = F(n-1) + F(n-2),问题边界F(1)=1,F(2)=1。此时可以很容易写出如下递归代码:

def Fibonacci(n):
    if n <= 2:
        return 1
    return Fibonacci(n-1) + Fibonacci(n-2)

B) 展开递归路径:

我们将上述递归过程展开成如下执行路径,我们发现存在大量的重复计算,比如F(3)的分支在F(4)、F(5)、F(6)等中都有重复计算。

 C) 确定合适问题执行顺序

我们按n=1,2,3,…,N的顺序来计算,就能保存F(n)的所有依赖的子问题F(n-1) 和F(n-2)都在其执行前完成计算。

D) 确定合适的数据结构保存子问题的结果

我们可以采用数组的形式来保存子问题的结果,比如D[n]表示F(n)的结果。

E) 确定问题边界和问题计算逻辑

D[0]=F(0) = 0, D[1]=1, D[2]=1

D[n] = D[n-1] + D[n-2] \text{ if } n >= 3

此时我们可以写出动态规划的代码

def Fibonacci(n):
    D=[0 for i in range(n+1)]
    D[1], D[2]=1, 1
    for i in range(3, n + 1):
        D[i]=D[i-1]+D[i-2]
    return D[n]

F) 对时间和空间的优化分析

通过上述的代码可以发现,实际并不需要一个数组来保存全部的结果,只需要3个变量就可以完成。

def Fibonacci(n):
    D1, D2 = 1, 1
    for i in range(3, n + 1):
        D3 = D1 + D2
        D1 = D2
        D2 = D3
    return D3

另外也可以发现在执行上是否不需要遍历n个,确实是的,最优的执行时间复杂度为O(\log n),Jeff的原文给出了具体的方法,本文不再列出了。接下来,我们考虑更为复杂的问题

2. 正则匹配的问题

正则匹配问题定义为:

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。

'*' 可以匹配任意字符串(包括空字符串)。

两个字符串完全匹配才算匹配成功。

说明:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

A) 定义递归形式:

首先我们仍然是定义问题的递归形式,我们先将问题拆解成如下的子问题情况:

  • 当s[0]和p[0]完全匹配,此时s[0]和p[0]都是字母,此时我们只需要判断s[1:]和p[1:]是否是匹配
  • 当p[0]是?,对应肯定匹配s[0],此时也只需要判断s[1:]和p[1:]是否是匹配
  • 当p[0]是*,此时对应三种情况为空,只同s[0]匹配,同s[0]匹配

我们用代码描述上述过程更为清晰,isMatch函数是要解决问题函数,其判断输入s和p是否是正则匹配(True or False)

if s[0] == p[0]:
    return isMatch(s[1:], p[1:])
elif p[0] == "?":
    return isMatch(s[1:], p[1:])
elif p[0] == "*":
    return isMatch(s[1:], p[1:]) or isMatch(s[1:], p) or isMatch(s, p[1:])
return False

我们再考虑递归问题的边界:

  • 当s和p均为空的情况下,返回True
  • 当只s为空的情况下,p为空或者持续为*的情况,返回True否则为False
  • 当只p为空的情况下,返回False
if len(s) == 0:
    if p.strip("*") == "":
        return True
    else:
        return False
elif len(p) == 0:
    return False

B) 展开执行路径:

通过上面的分析,可以知道递归问题的依赖顺序可以表示为如下,此处的i,j分别代表s[i:],p[j:],F(i,j)表示s[i:],p[j:]是否是正则匹配。

F(i,j) \Leftarrow F(i+1, j+1), F(i, j+1), F(i+1, j)

C) 确定合适的执行路径:

执行顺序可以为如下,其中l_sl_p分别表示s和p的长度,此时能满足所有F(i,j)所依赖的子问题在其执行前完成执行。

i: l_s \rightarrow 0,\ j: l_p \rightarrow 0

D) 确定合适的数据结构保存子问题的结果:

我们可以采用矩阵的形式来保存子问题的结果,比如M[i][j]表示F(i,j)的结果,即s[i:],p[j:]是否是正则匹配。

E) 确定问题边界和问题计算逻辑:

  • i=l_s时即s为空,并且当j=l_p时即p为空,M[i][j] = True
  • 仅当i=l_s时,p持续为*时为True,否则为False
  • 仅当j=l_p时,M[i][j] = False
M = [[False for j in range(len(p) + 1)] for i in range(len(s) + 1)]
for j in range(len(p), -1, -1):
    if j == len(p) or p[j] == “*”:
        M[len(s)][j] = True
    else:
        break

M的计算逻辑,参考原来的递归问题的处理结构:

if s[i] == p[j]:
    M[i][j] = M[i-1][j-1]
elif p[j] == "?":
    M[i][j] = M[i-1][j-1]
elif p[j] == "*":
    M[i][j] = M[i-1][j-1] or M[i-1][j] or M[i][j-1]
else:
    M[i][j] = False

最终完成代码如下:

def isMatch(s, p):
    M = [[False for j in range(len(p) + 1)] for i in range(len(s) + 1)]
    for j in range(len(p), -1, -1):
        if j == len(p) or p[j] == "*":
            M[len(s)][j] = True
        else:
            break
    for i in range(len(s) - 1, -1, -1):
        for j in range(len(p) - 1, -1, -1):
            if s[i] == p[j]:
                M[i][j] = M[i+1][j+1]
            elif p[j]== "?":
                M[i][j] = M[i+1][j+1]
            elif p[j] == "*":
                M[i][j] = M[i+1][j+1] or M[i+1][j] or M[i][j+1]
            else:
                M[i][j] = False
    return M[0][0]

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

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

相关文章

分页数据渲染

SearchResult属性增加 增加属性 private List<Integer> pageNavs; 目的&#xff1a;产生分页效果&#xff0c;方便操作&#xff0c;navs记录了从1到总页数的暑假 不是第一页时显示上一页 <a class"page_a" th:attr"pn${result.pageNum - 1}" hre…

算法基础集训(第30天)------>DFS之经典【n皇后问题】

一&#xff1a;概念定义n−皇后问题是指将 n个皇后放在 nn的国际象棋棋盘上&#xff0c;使得皇后不能相互攻击到&#xff0c;即任意两个皇后都不能处于同一行、同一列或同一斜线上。二&#xff1a;题目描述n−皇后问题是指将 n个皇后放在 nn 的国际象棋棋盘上&#xff0c;使得皇…

【C++入门】函数重载

目  录1 函数重载概念2 C支持函数重载的原理 -- 名字修饰&#xff08;name Mangling&#xff09;1 函数重载概念 函数重载&#xff1a; 函数的一种特殊情况&#xff0c;C允许在同一作用域中声明几个功能类似的同名函数&#xff0c;这些同名函数的形参列表&#xff08;参数个数…

企企通入选「AI中国」机器之心“最具商业价值解决方案 TOP 30” 榜单

近日&#xff0c;由专业的人工智能信息服务平台机器之心&#xff0c;发起并评选的「AI 中国」机器之心2022 年度评选榜单正式公布&#xff0c;企企通凭借过去一年在采购供应链领域取得的成就&#xff0c;以及在技术、产品、服务能力方面具备的独特优势&#xff0c;从众多企业中…

Python连接Liunx中mysql数据库-三表查询【10个经典案例】

关于Python连接liunx中mysql数据库的方式在这一篇文章 Python连接Liunx中mysql数据库-保姆级教程 关于Python针对liunx中的mysql数据库进行增删改查操作的文章在这一篇可以看一下 Python连接Liunx中mysql数据库-增删改查 对于单表查询的学习可以看这一篇文章 Python对liunx中my…

使用.ibd文件恢复Mysql数据库数据

使用.ibd文件恢复Mysql数据库数据问题发现问题解决第一步&#xff1a;查找mysql数据目录第二步&#xff1a;创建表第三步&#xff1a;解除表空间第四步&#xff1a;复制原数据库.ibd文件第五步&#xff1a;导入表空间ERROR 1030 (HY000): Got error 194 “Tablespace is missin…

使用Navicat生成MySQL测试数据

使用Navicat生成MySQL测试数据 Navicat版本&#xff1a;16.1.3 场景&#xff1a;因为某些原因&#xff0c;本人负责项目的数据库需要从MySQL迁移到PostgreSQL&#xff0c;所以就想测试一下PostgreSQL数据库的分区表性能。测性能的话大概需要两千万的测试数据&#xff0c;从生…

蓝队--Linux基线安全检查

文章目录认证账号和口令安全创建用户用户安全基线授权文件系统安全审计日志安全日志存放的位置设备等级观察一条日志产生的过程网络安全网络协议与服务安全SSH的配置防火墙安全操作系统SELinux登陆安全kali忘记密码怎么办怎么物理加密进入BOIS界面选择Security选择set User Pas…

【前端】css样式视口、布局

一、 视口概念 视口&#xff08;viewport&#xff09;是用来约束网页中最顶级块元素的&#xff0c;即它决定了网页的大小&#xff0c;网页是先在视口上渲染&#xff0c;然后再通过视口放回到浏览器窗口上的&#xff0c;网页的渲染过程如下&#xff1a; pc端视口的大小和浏览器…

Python---类与对象

专栏&#xff1a;python 个人主页&#xff1a;HaiFan. 专栏简介&#xff1a;本专栏主要更新一些python的基础知识&#xff0c;也会实现一些小游戏和通讯录&#xff0c;学时管理系统之类的&#xff0c;有兴趣的朋友可以关注一下。 类与对象前言定义类创建对象前言 面向对象跟函…

河北如何推进农村生活污水处理?

自“十四五”以来&#xff0c;我国各地都针对地方农村人居环境的改善颁布相关方案&#xff0c;力图改善农村生活污水处理这一自本世纪初就始终困扰我国广大农村居民的难题。今天&#xff0c;小编就为大家介绍一下&#xff0c;湖北省针对农村污水处理所采取的最新措施。 河北地…

统计图表有哪几个种类及其特点

关于数据分析中常用统计图表的分类和各种图表的特点&#xff0c;这一张图就能概括&#xff01; 下面来展开说下&#xff0c;为了直观展示&#xff0c;给大家找了一份可视化统计图表的模板&#xff0c;可直接在线使用&#xff0c;需要的小伙伴自取哈。 点此领取模板>>htt…

端口映射和端口转发

文章目录端口转发定义端口映射的定义端口转发简介常见转发工具LCX1、目标机有公网lP2、端口映射3、目标机无公网IP端口转发和端口映射都是为了解决内网主机的端口无法在外部直接访问而衍生出来的技术&#xff0c;通过中间服务器进行中转&#xff0c;将内部的端口映射到公网IP上…

【AI-NLP】Transformer理论及源码理解

文章目录提出背景总体结构EncoderDecoder细节再探源码阅读主函数部分主模型定义编码器编码层多头自注意力层前馈神经网络层Masked解码器解码层参考资料提出背景 改进了RNN的训练慢的缺点&#xff0c;利用self-attention机制实现并行计算。并且Transformer可以增加到非常深的深…

用css实现简易报警灯

主题 用css来实现一个简易的报警灯效果 实现效果 实现思路 实现的核心是一个灯罩和一个灯芯。灯罩主要是使用了border-radius圆角边框&#xff0c;灯芯主要是radial-gradient径向渐变。再使用动画效果来实现一闪一闪的效果。让我们来一步一步实现效果。 灯罩实现 因为大…

剑指Offer 第18天 I. 二叉树的深度 II. 平衡二叉树

目录 剑指 Offer 55 - I. 二叉树的深度 剑指 Offer 55 - II. 平衡二叉树 剑指 Offer 55 - I. 二叉树的深度 输入一棵二叉树的根节点&#xff0c;求该树的深度。从根节点到叶节点依次经过的节点&#xff08;含根、叶节点&#xff09;形成树的一条路径&#xff0c;最长路径的长度…

2025年突破百万套/年,又一个前装赛道成为行业新风口

汽车智能化&#xff0c;不仅仅是增量&#xff0c;还有存量升级。 作为驾驶员观察道路状况的关键车载部件&#xff0c;汽车后视镜&#xff08;基于凸面镜&#xff09;最早出现在1906年&#xff0c;随后逐步成为所有车辆的基本标配。不过&#xff0c;由于镜面设计的缺陷&#xf…

【ROS学习】节点运行管理launch文件的基本操作

launch文件的概念和作用 launch 文件是一个 XML 格式的文件&#xff0c;可以启动本地和远程的多个节点&#xff0c;还可以在参数服务器中设置参数。 launch文件的作用是&#xff1a;简化节点的配置与启动&#xff0c;提高ROS程序的启动效率。 使用场景 launch文件在ros中使用还…

mybatis中获取插入数组的主键值(自增主键,非增主键),mp

1.自增主键的第一种写法 <insert id"addKey" ><!--通过mybatis框架提供的selectKey标签获得自增产生的ID值--><selectKey resultType"java.lang.Integer" order"AFTER" keyProperty"id">select LAST_INSERT_ID()<…

【Azure 架构师学习笔记】-Azure Logic Apps(5)- 标准和使用量类型的区别

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Logic Apps】系列。 接上文【Azure 架构师学习笔记】-Azure Logic Apps&#xff08;4&#xff09;-演示2 前言 在做实验的过程中&#xff0c;发现使用“使用量”&#xff08;Consumption)类型会出现很多问题&#xff…