算法学习笔记(8.2)-动态规划入门进阶

目录

问题判断:

问题求解步骤:

图例:

解析:

方法一:暴力搜索

实现代码如下所示:

解析:

方法二:记忆化搜索

代码示例:

解析:

方法三:动态规划

空间优化

代码如下

问题判断:

总的来说,如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常适合用动态规划求解。然而,我们很难从问题的描述中直接提取出这些特征,因此通常需要放宽条件,首先需要观察问题适不适合使用回溯(穷举)解决。

适合用回溯解决的问题通常满足“决策树模型”,对于问题可以使用树形结构来描述,其中每一个节点代表一个决策,每一条路径代表一个决策序列。

换句话说,如果问题包含明确的决策概念,并且解是通过一系列决策产生的,那么它就满足决策树模型,通常可以使用回溯解决。

在此基础上,动态规划问题还有一些判断的“加分项”。

1. 问题包含最大(小)或最多(少)等优化描述。

2. 问题的状态能够使用一个列表、多维矩阵或树来表示,并且一个状态与其周围的状态存在递推关系。

相应地,也存在一些“减分项”。

1. 问题的目标是找出所有的解决方案,而不是找出最优解。

2. 问题描述中有明显的排列组合特征,需要返回具体的对个方案。

如果一个问题满足决策树模型,并具有较为明显的“加分项”,我们就可以假设它是一个动态规划问题,并在求解过程中验证它。

问题求解步骤:

动态规划的问题解题流程会因为问题的性质和难度有所不同,但通常遵循一下步骤:描述决策,定义状态,建立dp表,推导状态转移方程,确定边界问题等。

为了理解动态规划的解题的过程,使用一个经典的例题“最短路径和”

Question

给定一个n * m的二维网格grid,网格中的每个单元包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回左上角到右下角的最小路径和。

图例:

给定网格的最小路径和为13

第一步:思考每轮的决策,定义状态,从而得到dp表

本题的每一轮的决策就是从当前格子向下或者向右走一步。设当前格的行列索引为[i,j],则向下或向右走一步后,索引变为[i+1,j]或[i,j+1]。因此,状态应包含行索引和列索引两个变量,记为[i,j]。

状态[i,j]对应的子问题为:从起始点[0,0]走到[i,j]的最小路径和,记作dp[i,j]。

如图所示:dp二维矩阵,其尺寸与输入网格grid相同。

注意点:(Note)

动态规划和回溯过程可以描述成为一个决策序列,而状态由所有决策变量构成。应当包含描述解题进度的所有变量,其包含了足够的信息,能用来推导下一个状态。

每个状态都对应一个子问题,我们会定义成为一个dp表来存储所有子问题的解,状态的每个独立变量都是dp表中的一个维度。从本质上看,dp表是状态和子问题的解之间的映射。

第二步:找出最优子结构,进而推导出状态转移方程

对应状态[i,j],它只能从上边的格子[i-1,j]和左边的格子[I,j-1]转移过来。因此最优子结构为:到达[i,j]的最小路径和由[i-1,j]的最小路径和与[I,j-1的最小路径和哪个较小来决定。

根据以上的分析得出状态转移方程为:

dp[i,j] = min(dp[i-1,j],dp[i,j-1]) +grid[i,j]

图示如下:

注意点:(Note)

根据定义好的dp表,思考原问题和子问题的关系,找出通过子问题的最优解来构造原问题的最优解的方法,即最优子结构。

一旦我们找到了最优子结构,就可以使用它来构建出来状态转移方程。

第三步:确定边界条件和状态转移顺序

在本题中,处在首行的状态只能从其左边的状态得来,处在首列的状态只能从其上边的状态得来,因此首行i = 0 和首列的 j = 0 是边界条件。

如图所示,由于每个格子是由其左方格子和上方格子转移而来,因此我们使用循环来遍历矩阵,外循环遍历各行,内循环遍历各列。

解析:

根据对上面的理解我们已经可以写出动态规划的代码。然而子问题的解决是一种从顶至底的思想,因此按照“暴力搜索”->“记忆化搜索”->“动态规划”的顺序实现符合思维习惯。

方法一:暴力搜索

从状态[i,j]开始搜索,不断分解为更小的状态[i-1,j]和[i,j-1],递归函数包括以下的要素。

  1. 递归参数:状态[i,j]。
  2. 返回值:从[0,0]到[i,j]的最小路径和dp[i,j]。
  3. 终止条件:当 i = 0 且 j = 0 时,返回代价grid[0,0]。
  4. 剪枝:当i<0时或j<0时索引越界时,此时返回代价+∞,代表不可行。
实现代码如下所示
# python 代码示例
def min_path_sum_dfs(grid, i, j) :
    if i == 0 and j == 0 :
        return grid[0][0]
    if i < 0 or j < 0 :
        return inf
    up = min_path_sum_dfs(grid, i - 1, j)
    left = min_path_sum_dfs(grid, i, j - 1)
    return min(up, left) + grid[i][j]
// c++ 代码示例
int minPathSumDFS(vector<vector<int>> &grid, int i, int j)
{
    if (i == 0 && j == 0)
    {
        return grid[0][0] ;
    }    
    if (i < 0 or j < 0)
    {
        retunr INT_MAX ;
    }
    int up = minPathSumDFS(grid, i - 1, j) ;
    int left = minPathSumDFS(grid, i, j - 1) ;
    return min(up, left) + gird[i][j] ;
}

解析:

给出一个dp[2,1]为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格grid的尺寸变大而急剧增多。

造成重叠子问题的原因:存在多条路径可以从左上角到达某一单元格。

每个状态都有向下和向右两种选择,从左上角走到右下角总共需要m+n-2步,所以最差的时间复杂度为O(2^(m+n))。这种计算方法并没有考虑网格的边界情况,当到达网格边界时只剩下一种选择,因此实际的路径会少一些。

方法二:记忆化搜索

引入一个与grid网格大小相同的记忆列表mem,用于记录各个子问题的解,并将重叠子问题进行剪枝。

代码示例:
// c++ 代码示例
def min_path_sum_dfs_mem(grid, mem, i, j) :
    if i == 0 and j == 0 :
        return grid[0][0]
    if i < 0 or j < 0 :
        return inf
    if mem[i][j] != -1 :
        return mem[i][j]
    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)
    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)
    mem[i][j] = min(up, left) + grid[i][j]
    return mem[i][j]
// c++ 代码示例
int minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {
    if (i == 0 && j == 0) {
        return grid[0][0] ;
    }
    if (i < 0 || j < 0) {
        return INT_MAX ;
    }
    if (mem[i][j] != -1) {
        return mem[i][j] ;
    }
    int up = minPathSumDFSMem(grid, mem, i - 1, j) ; 
    int left = minPathSumDFSMem(grid, mem, i, j - 1) ;
    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX ;
    return mem[i][j] ;
}

解析:

在引入记忆化搜索之后,所有的子问题只需要计算一次,因此时间复杂度取决于状态总数,即网格尺寸O(m*n)

方法三:动态规划

基于迭代实现动态规划,代码如下所示:

# pyhton 代码示例
def min_path_sum_dp(grid) :
    n, m = len(grid), len(grid[0])
    dp = [ [0] * m for _ in range(n)]
    dp[0][0] = grid[0][0]
    for j in range(1, m) :
        dp[0][j] = dp[0][j - 1] + grid[0][j]
    for i in range(1, n) :
        dp[i][0] = dp[i - 1][0] + grid[i][0]
    for i in range(1, n) :
        for j in range(1, m) :
            dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + gird[i][j]
    return dp[n - 1][m - 1]
int minPathSumDP(vector<vector<int>> &grid) {
    int n = grid.size(), m = grid[0].size();
    vector<vector<int>> dp(n, vector<int>(m));
    dp[0][0] = grid[0][0];
    for (int j = 1; j < m; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    for (int i = 1; i < n; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < m; j++) {
            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    return dp[n - 1][m - 1];
}

动态规划的过程如下所示:便利了整个网络,因此时间复杂度为O(m*n)。

数组的dp的大小也为m * n ,因此空间复杂度为O(m*n)。

空间优化

由于每个格子只与左边和上边的格子有关,我们可以只用一个单行数组来实现dp表。

关键:数组dp只能表示一行的状态,我们无法提前初始化首行的状态,而是在遍历每行时更新它。

代码如下:
# python 代码示例

def min_path_sum_dp_comp(grid : list[list[int]]) -> int :
    n, m = len(grid), len(grid[0])
    dp = [0] * m
    dp[0] = grid[0][0]
    for j in range(1, m) :
        dp[j] = dp[j - 1] + grid[0][j]
    for i in range(1, n) :
        dp[0] = dp[0] + grid[i][0]
        for j in range(1, m) :
            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]
    return dp[m - 1]
// c++ 代码示例
int minPathSumDPComp(vector<vector<int>> &grid)
{
    int n = grid.size(), m = grid[0].size() ;
    vector<int> dp(m) ;
    dp[0] = grid[0][0] ;
    for (int j = 1 ; j < m ; j++)
    {
        dp[j] = dp[j - 1] + gird[0][j] ;
    }
    for (int i = 1 ; i < n ; i++)
    {
        dp[0] = dp[0] + grid[i][0] ;
        for (int j = 1 ; j < m ; j++)
        {
            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j] ;
        }
    }
    return dp[m- 1] ;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

全球激光位移传感器市场规模逐渐扩大 企业数量不断增多

全球激光位移传感器市场规模逐渐扩大 企业数量不断增多 位移传感器又称为距离传感器&#xff0c;是用于测量与被测物体之间距离的一种传感器。根据工作原理&#xff0c;位移传感器可以分为超声波位移传感器、红外线位移传感器、激光位移传感器等。其中&#xff0c;激光位移传感…

2024年06月CCF-GESP编程能力等级认证Python编程五级真题解析

本文收录于专栏《Python等级认证CCF-GESP真题解析》&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 一、单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 第 1 题 在Python中&#xff0c;print((c for c in “GESP”))的输…

vue实例和容器的一夫一制——04

//准备容器 <div classapp> <h1>{{mag}}</h1> </div> //准备容器 <div classapp> <h1>{{mag}}</h1> </div> //准备容器 <div classapp2> <h1>{{name}}</h1> </div> <script> // 验…

深度整合全球资源,分贝通打造高效、合规的海外差旅管理平台

在全球化商业活动的背景下,中国企业出海已成为常态。然而,随着海外差旅市场的全面增长,企业在海外支出管理上面临诸多挑战。据2023年数据显示,分贝通出海差旅业务GMV同比增长高达500倍,这一增长背后隐藏着企业对于更省钱、更高效管控方式的迫切需求。 面对与日俱增的开支,企业开…

嵌入式代码升级——IAP

目录 IAP的特点 实现 IAP 功能 STM32 正常的程序运行流程 STM32 加入IAP后的运行流程 程序执行流程 BootLoader程序 APP1程序 APP2程序 验证操作步骤 IAP&#xff08;In-Application Programming&#xff09;指的是在应用程序运行时对其自身的Flash存储器进行编程的操作…

LLM - Transformer 的 多头自注意力(MHSA) 理解与源码

欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/140281680 免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。 在 Transformer 中,多头自注意力机制 (MHSA, Multi-Head Self-Attenti…

【pytorch18】Logistic Regression

回忆线性回归 for continuous:y xwbfor probability output:yσ(xwb) σ:sigmoid or logistic 线性回归是简单的线性模型&#xff0c;输入是x&#xff0c;网络参数是w和b&#xff0c;输出是连续的y的值 如何把它转化为分类问题?加了sigmoid函数&#xff0c;输出的值不再是…

Gen4Gen:多概念个性化图像生成的数据驱动革新

个性化文本到图像生成模型在用户控制生成过程方面取得了重要进展。这些模型能够通过少量训练样本学习并合成包含新颖个性化概念的图像&#xff0c;例如用户的宠物或特定物品。然而&#xff0c;现有技术在处理多概念个性化时存在局限性&#xff0c;尤其是在生成包含多个相似概念…

挂耳式耳机哪款比较好、挂耳式耳机推荐高性价比

近年来&#xff0c;开放式耳机行业蓬勃发展&#xff0c;受到了越来越多消费者的喜爱&#xff0c;然而&#xff0c;这里边也夹着不专业的产品&#xff0c;低质量的生产不仅不能带来舒适的体验&#xff0c;甚至可能对耳朵造成潜在的伤害。挂耳式耳机哪款比较好&#xff1f;为了帮…

初识STM32:寄存器编程 × 库函数编程 × 开发环境

STM32的编程模型 假如使用C语言的方式写了一段程序&#xff0c;这段程序首先会被烧录到芯片当中&#xff08;Flash存储器中&#xff09;&#xff0c;Flash存储器中的程序会逐条的进入CPU里面去执行。 CPU相当于人的一个大脑&#xff0c;虽然能执行运算和执行指令&#xff0c;…

WPF依赖附加属性

依赖附加属性的定义 基本过程&#xff1a;声明、注册、包装 依赖附加属性必须在依赖对象&#xff0c;附加属性不一定&#xff0c;关注的是被附加的对象是否是依赖对象 快捷方式&#xff1a;propa tab 关键字&#xff1a;RegisterAttached // 方法封装 public static int …

Java客户端调用SOAP方式的WebService服务实现方式分析

简介 在多系统交互中&#xff0c;有时候需要以Java作为客户端来调用SOAP方式的WebService服务&#xff0c;本文通过分析不同的调用方式&#xff0c;以Demo的形式&#xff0c;帮助读者在生产实践中选择合适的调用方式。 本文JDK环境为JDK17。 结论 推荐使用Axis2或者Jaxws&#…

推出全新的无线通讯模块(1SJ型、2DT-158型、2GT-001型、1YN型、2AE型)助力物联网新发展

相关型号&#xff1a;LBAA0QB1SJ-296 LBAA0XV2DT-158 LBAA0XV2GT-001 LBEE5KL1YN-814 LBEE5PK2AE-564 全新的无线通讯模块&#xff08;1SJ型、2DT-158型、2GT-001型、1YN型、2AE型&#xff09;助力物联网新发展&#xff08;明佳达&#xff09; 1、1SJ型集成LoRaWAN调制解调器…

优劣分析:重启路由器 vs 使用IP代理

目前更换IP主要有两种常见方法&#xff0c;一种是重启路由器&#xff0c;另一种是使用代理IP&#xff0c;那么&#xff0c;这两种方法有什么优缺点呢&#xff1f;下面我们一起来探讨一下。 方法一&#xff1a;重启路由器变换IP 优点 1. 操作简单&#xff1a;只需断开路由器电…

『C + ⒈』‘\‘

&#x1f942;在反斜杠(\)有⒉种最常用的功能如下所示&#x1f44b; #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main(void) {int a 10;int b 20;int c 30;if (a 10 &&\b 20 &&\c 30){printf("Your print\n");}else{prin…

解锁AI大模型潜能:预训练、迁移学习与中间件编程的协同艺术

在人工智能的浩瀚星空中&#xff0c;大型预训练模型&#xff08;Large Language Models, LLMs&#xff09;犹如璀璨的星辰&#xff0c;引领着技术革新的浪潮。这些模型通过海量数据的滋养&#xff0c;学会了理解语言、生成文本乃至执行复杂任务的能力。然而&#xff0c;要让这些…

可以拖拽的富文本编辑器(VueDragResize,quill-editor)

该功能实现一个帮助文档的展示和编辑功能&#xff0c;默认进去只能查看帮助文档的内容&#xff0c;点击编辑可以进行富文本编辑器的编辑功能。 出现的问题1.如何隐藏富文本编辑的工具栏并且禁止编辑 //隐藏工具栏this.toolbar this.$refs.myTextEditor.quill.getModule(toolb…

LeetCode之无重复字符的最长子串

1.题目链接 3. 无重复字符的最长子串 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/ 2.题目解析 题目主要思路其实是滑动窗口&#xff0c;使用两个指针维护一个动态区间&#xff0c;使…

价格疑云?格行WiFi创始人亲解谜团,性价比之王如何炼成?

随身wifi行业乱象频出&#xff0c;作为行业领跑品牌的格行随身wifi&#xff0c;关于价格问题一直备受质疑。关于设备上的“格行自有格行的骄傲”也被外界认定为是自大&#xff0c;甚至发展的线下一万多家门店也被同行不认可。近日&#xff0c;企业财经专访记者有幸采访了格行随…

实时消息推送系统,写得太好了!

websocket 协议是在 http 协议上的一种补充协议&#xff0c;是 html5 的新特性&#xff0c;是一种持久化的协议。其实 websocket 和 http 关系并不是很大&#xff0c;不过都是属于应用层的协议&#xff0c;接下来我们就开始实战。 websocket 定时推送 本教程基于 springboot …