数据结构与算法(十):动态规划与贪心算法

参考引用

  • Hello 算法
  • Github:hello-algo

1. 动态规划算法

  • 动态规划将一个问题分解为一系列更小的子问题,并通过存储子问题的解来避免重复计算,从而大幅提升时间效率

问题:给定一个共有 n 阶的楼梯,你每步可以上 1 阶或者 2 阶,请问有多少种方案可以爬到楼顶?

  • 下图所示,对于一个 3 阶楼梯,共有 3 种方案可以爬到楼顶

在这里插入图片描述

  • 本题的目标是求解方案数量,可以考虑通过回溯来穷举所有可能性。具体来说,将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上 1 阶或 2 阶,每当到达楼梯顶部时就将方案数量加 1,当越过楼梯顶部时就将其剪枝
    /* 回溯 */
    void backtrack(vector<int> &choices, int state, int n, vector<int> &res) {// 当爬到第 n 阶时,方案数量加 1if (state == n)res[0]++;// 遍历所有选择for (auto &choice : choices) {// 剪枝:不允许越过第 n 阶if (state + choice > n)break;// 尝试:做出选择,更新状态backtrack(choices, state + choice, n, res);// 回退}
    }/* 爬楼梯:回溯 */
    int climbingStairsBacktrack(int n) {vector<int> choices = {1, 2}; // 可选择向上爬 1 或 2 阶int state = 0;                // 从第 0 阶开始爬vector<int> res = {0};        // 使用 res[0] 记录方案数量backtrack(choices, state, n, res);return res[0];
    }
    

1.1 方法一:暴力搜索

  • 回溯算法通常并不显式地对问题进行拆解,而是将问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。可以尝试从问题分解的角度分析这道题。设爬到第 i i i 阶共有 d p [ i ] dp[i] dp[i] 种方案,那么 d p [ i ] dp[i] dp[i] 就是原问题,其子问题包括

d p [ i − 1 ] , d p [ i − 2 ] , … , d p [ 2 ] , d p [ 1 ] dp[i-1],dp[i-2],\ldots,dp[2],dp[1] dp[i1],dp[i2],,dp[2],dp[1]

  • 由于每轮只能上 1 阶或 2 阶,因此当站在第 i 阶楼梯上时,上一轮只可能站在第 i-1 阶或第 i-2 阶上。换句话说,只能从第 i-1 阶或第 i-2 阶前往第 i 阶
    • 由此便可得出一个重要推论:爬到第 i-1 阶的方案数加上爬到第 i-2 阶的方案数就等于爬到第 i 阶的方案数
    • 在爬楼梯问题中,各子问题之间存在递推关系,原问题的解可以由子问题的解构建得来,下图展示了该递推关系

d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i1]+dp[i2]

在这里插入图片描述

  • 可以根据递推公式得到暴力搜索解法
    • d p [ n ] dp[n] dp[n] 为起始点,递归地将一个较大问题拆解为两个较小问题的和,直至到达最小子问题 d p [ 1 ] dp[1] dp[1] d p [ 2 ] dp[2] dp[2] 时返回。其中,最小子问题的解是已知的,即 d p [ 1 ] = 1 dp[1]=1 dp[1]=1 d p [ 2 ] = 2 dp[2] = 2 dp[2]=2,表示爬到第 1、2 阶分别有 1、2 种方案
    /* 搜索 */
    int dfs(int i) {// 已知 dp[1] 和 dp[2] ,返回之if (i == 1 || i == 2)return i;// dp[i] = dp[i-1] + dp[i-2]int count = dfs(i - 1) + dfs(i - 2);return count;
    }/* 爬楼梯:搜索 */
    int climbingStairsDFS(int n) {return dfs(n);
    }
    
  • 下图展示了暴力搜索形成的递归树。对于问题 d p [ n ] dp[n] dp[n],其递归树的深度为 n,时间复杂度为 O ( 2 n ) O(2^n) O(2n)
    • 指数阶属于爆炸式增长,如果输入一个比较大的 n,则会陷入漫长的等待之中
    • 指数阶的时间复杂度是由于 “重叠子问题” 导致的,以此类推,子问题中包含更小的重叠子问题,子子孙孙无穷尽也,绝大部分计算资源都浪费在这些重叠的问题上

在这里插入图片描述

1.2 方法二:记忆化搜索

  • 为了提升算法效率,希望所有的重叠子问题都只被计算一次。为此,声明一个数组 mem 来记录每个子问题的解,并在搜索过程中将重叠子问题剪枝
    • 当首次计算 d p [ i ] dp[i] dp[i] 时,将其记录至 mem[i],以便之后使用
    • 当再次需要计算 d p [ i ] dp[i] dp[i] 时,便可直接从 mem[i] 中获取结果,从而避免重复计算该子问题
    /* 记忆化搜索 */
    int dfs(int i, vector<int> &mem) {// 已知 dp[1] 和 dp[2] ,返回之if (i == 1 || i == 2)return i;// 若存在记录 dp[i] ,则直接返回之if (mem[i] != -1)return mem[i];// dp[i] = dp[i-1] + dp[i-2]int count = dfs(i - 1, mem) + dfs(i - 2, mem);// 记录 dp[i]mem[i] = count;return count;
    }/* 爬楼梯:记忆化搜索 */
    int climbingStairsDFSMem(int n) {// mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录vector<int> mem(n + 1, -1);return dfs(n, mem);
    }
    
  • 下图所示,经过记忆化处理后,所有重叠子问题都只需被计算一次,时间复杂度被优化至 O(n)
    • 记忆化搜索是一种 “从顶至底” 的方法,从原问题(根节点)开始,递归地将较大子问题分解为较小子问题,直至解已知的最小子问题(叶节点)。之后,通过回溯将子问题的解逐层收集,构建出原问题的解

在这里插入图片描述

1.3 方法三:动态规划

  • 动态规划是一种 “从底至顶” 的方法:从最小子问题的解开始,迭代地构建更大子问题的解,直至得到原问题的解
    • 由于动态规划不包含回溯过程,因此只需使用循环迭代实现,无须使用递归。在以下代码中,初始化一个数组 dp 来存储子问题的解
    /* 爬楼梯:动态规划 */
    int climbingStairsDP(int n) {if (n == 1 || n == 2)return n;// 初始化 dp 表,用于存储子问题的解vector<int> dp(n + 1);// 初始状态:预设最小子问题的解dp[1] = 1;dp[2] = 2;// 状态转移:从较小子问题逐步求解较大子问题for (int i = 3; i <= n; i++) {dp[i] = dp[i - 1] + dp[i - 2];}return dp[n];
    }
    

在这里插入图片描述

根据以上内容,总结出动态规划的常用术语

  • 将数组 d p dp dp 称为 d p dp dp 表, d p [ i ] dp[i] dp[i] 表示状态 i i i 对应子问题的解
  • 将最小子问题对应的状态(即第 1 和 2 阶楼梯)称为初始状态
  • 将递推公式 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i1]+dp[i2] 称为状态转移方程

1.4 空间优化

  • 由于 d p [ i ] dp[i] dp[i] 只与 d p [ i − 1 ] dp[i-1] dp[i1] d p [ i − 2 ] dp[i-2] dp[i2] 有关,因此无须使用一个数组 dp 来存储所有子问题的解,而只需两个变量滚动前进即可
    /* 爬楼梯:空间优化后的动态规划 */
    // 空间复杂度从 O(n) 降低至 O(1)
    int climbingStairsDPComp(int n) {if (n == 1 || n == 2)return n;int a = 1, b = 2;for (int i = 3; i <= n; i++) {int tmp = b;b = a + b;a = tmp;}return b;
    }
    

在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时可以只保留必要的状态,通过 “降维” 来节省内存空间,这种空间优化技巧被称为 “滚动变量” 或 “滚动数组”

2. 贪心算法

  • 贪心算法是一种常见的解决优化问题的算法,其基本思想是:在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期望获得全局最优解

  • 贪心算法和动态规划都常用于解决优化问题,它们之间的区别如下

    • 动态规划会根据之前阶段的所有决策来考虑当前决策,并使用过去子问题的解来构建当前子问题的解
    • 贪心算法不会重新考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直至问题被解决

问题:给定 n 种硬币,第 i 种硬币的面值为 coins[i-1],目标金额为 amt,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数,如果无法凑出目标金额则返回 -1

在这里插入图片描述

/* 零钱兑换:贪心 */
int coinChangeGreedy(vector<int> &coins, int amt) {// 假设 coins 列表有序int i = coins.size() - 1;int count = 0;// 循环进行贪心选择,直到无剩余金额while (amt > 0) {// 找到小于且最接近剩余金额的硬币while (i > 0 && coins[i] > amt) {i--;}// 选择 coins[i]amt -= coins[i];count++;}// 若未找到可行方案,则返回 -1return amt == 0 ? count : -1;
}

2.1 贪心算法优缺点

  • 贪心算法不仅操作直接、实现简单,而且通常效率也很高。在以上代码中,记硬币最小面值为 m i n ( c o i n s ) min(coins) min(coins),则贪心选择最多循环 a m t / m i n ( c o i n s ) amt/min(coins) amt/min(coins) 次,时间复杂度为 O ( a m t / m i n ( c o i n s ) ) O(amt/min(coins)) O(amt/min(coins))。这比动态规划解法的时间复杂度 O ( n ∗ a m t ) O(n*amt) O(namt) 提升了一个数量级

  • 然而,对于某些硬币面值组合,贪心算法并不能找到最优解

    • 正例 c o i n s = [ 1 , 5 , 10 , 20 , 50 , 100 ] coins = [1, 5, 10, 20, 50, 100] coins=[1,5,10,20,50,100]:在该硬币组合下,给定任意 a m t amt amt,贪心算法都可以找出最优解
    • 反例 1 c o i n s = [ 1 , 20 , 50 ] coins = [1, 20, 50] coins=[1,20,50]:假设 a m t = 60 amt = 60 amt=60,贪心算法只能找到 50 + 1 × 10 50 + 1×10 50+1×10 的兑换组合,共计 11 枚硬币,但动态规划可以找到最优解 20 + 20 + 20 20 + 20 + 20 20+20+20,仅需 3 枚硬币
    • 反例 2 c o i n s = [ 1 , 49 , 50 ] coins = [1, 49, 50] coins=[1,49,50]:假设 a m t = 98 amt = 98 amt=98,贪心算法只能找到 50 + 1 × 48 50 + 1×48 50+1×48 的兑换组合,共计 49 枚硬币,但动态规划可以找到最优解 49 + 49 49 + 49 49+49,仅需 2 枚硬币

在这里插入图片描述

  • 一般情况下,贪心算法适用于以下两类问题
    • 可以保证找到最优解:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效
    • 可以找到近似最优解:对于很多复杂问题来说,寻找全局最优解是非常困难的,能以较高效率找到次优解也是非常不错的

2.2 贪心典型例题

  • 硬币找零问题
    • 在某些硬币组合下,贪心算法总是可以得到最优解
  • 区间调度问题
    • 假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解
  • 分数背包问题
    • 给定一组物品和一个载重量,你的目标是选择一组物品,使得总重量不超过载重量,且总价值最大。如果每次都选择性价比最高(价值 / 重量)的物品,那么贪心算法在一些情况下可以得到最优解
  • 股票买卖问题
    • 给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润
  • 霍夫曼编码
    • 霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最小的两个节点合并,最后得到的霍夫曼树的带权路径长度(即编码长度)最小
  • Dijkstra 算法
    • 它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法

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

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

相关文章

【王道代码】【2.3链表】d1

关键字&#xff1a; 递归删除x&#xff1b;删除所有x&#xff1b;递归反向输出&#xff1b;删除最小结点&#xff08;2组指针&#xff09;&#xff1b;原地逆置&#xff1b;使递增有序

2008-2021年上市公司实体企业金融化程度测算数据(原始数据+stata代码)

2008-2021年上市公司实体企业金融化程度测算&#xff08;原始数据stata代码&#xff09; 1、时间&#xff1a;2008-2021年 2、指标&#xff1a;股票代码、年份、交易性金融资产、衍生金融资产、发放贷款及垫款净额、可供出售金融资产净额、持有至到期投资净额、长期债权投资净…

github 终端克隆操作,以及对 https/ssh 的理解

前言 最近瞎搞 github 的一些配置&#xff0c;结果搞得有一段时间克隆不了仓库。不过经历了这次风波后&#xff0c;我对 github 的一些原理有了更清楚的了解。所以想稍微写一小篇文章总结输出一下&#xff0c;也欢迎有疑问的读者与博主进一步交流&#xff0c;我的理解还是有限…

[1Panel]开源,现代化,新一代的 Linux 服务器运维管理面板

测评介绍 本期测评试用一下1Panel这款面板。1Panel是国内飞致云旗下开源产品。整个界面简洁清爽&#xff0c;后端使用GO开发&#xff0c;前端使用VUE的Element-Plus作为UI框架&#xff0c;整个面板的管理都是基于docker的&#xff0c;想法很先进。官方还提供了视频的使用教程&…

leetcode:2678. 老人的数目(python3解法)

难度&#xff1a;简单 给你一个下标从 0 开始的字符串 details 。details 中每个元素都是一位乘客的信息&#xff0c;信息用长度为 15 的字符串表示&#xff0c;表示方式如下&#xff1a; 前十个字符是乘客的手机号码。接下来的一个字符是乘客的性别。接下来两个字符是乘客的年…

leetcode:217. 存在重复元素(先排序再比较邻位)

一、题目&#xff1a; 函数原型&#xff1a; bool containsDuplicate(int* nums, int numsSize) 参数分析&#xff1a; nums是传入的数组 numsSize是传入数组的元素个数 二、思路&#xff1a; 根据题意&#xff0c;判断数组中是否存在出现两次以上的元素。可以先将数组排序&…

基于混沌博弈优化的BP神经网络(分类应用) - 附代码

基于混沌博弈优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码 文章目录 基于混沌博弈优化的BP神经网络&#xff08;分类应用&#xff09; - 附代码1.鸢尾花iris数据介绍2.数据集整理3.混沌博弈优化BP神经网络3.1 BP神经网络参数设置3.2 混沌博弈算法应用 4.测试结果…

【汇编语言特别篇】DOSBox及常用汇编工具的详细安装教程

文章目录 &#x1f4cb;前言一. ⛳️dosbox的介绍、下载和安装1.1 &#x1f514;dosbos简介1.2 &#x1f514;dosbox的下载1.2.1 &#x1f47b;方式一&#xff1a;官网下载(推荐)1.2.2 &#x1f47b;方式二&#xff1a;网盘安装包 1.3 &#x1f514;dosbox的安装1.4 &#x1f5…

CSS 滚动驱动动画 timeline-scope

timeline-scope 语法兼容性 timeline-scope 看到 scope 就知道这个属性是和范围有关, 没错, timeline-scope 就是用来修改一个具名时间线(named animation timeline)的范围. 我们介绍过的两种时间线 scroll progress timeline 和 view progress timeline, 使用这两种时间线(通…

TCP/IP网络分层模型

TCP/IP当初的设计者真的是非常聪明&#xff0c;创造性地提出了“分层”的概念&#xff0c;把复杂的网络通信划分出多个层次&#xff0c;再给每一个层次分配不同的职责&#xff0c;层次内只专心做自己的事情就好&#xff0c;用“分而治之”的思想把一个“大麻烦”拆分成了数个“…

Linux篇 五、Ubuntu与Linux板卡建立NFS服务

Linux系列文章目录 一、香橙派Zero2设置开机连接wifi 二、香橙派Zero2获取Linux SDK源码 三、香橙派Zero2搭建Qt环境 四、Linux修改用户名 文章目录 Linux系列文章目录前言一、连接到局域网互ping测试 二、安装NFS服务配置NFS更新exports配置三、板卡安装NFS客户端四、板卡临时…

LINUX | hexdump以16进制查看文件内容

LINUX | hexdump以16进制查看文件内容 时间&#xff1a;2023-10-20 文章目录 LINUX | hexdump以16进制查看文件内容1.参考2.示例1.以ASCII字符显示文件中字符2.以16进制和相应的ASCII字符显示文件里的字符3.只显示文件中前n个字符4.以偏移量开始格式输出 1.参考 1.Linux命令–h…

请问嵌入式或迁移学习要学什么?

请问嵌入式或迁移学习要学什么&#xff1f; 学习嵌入式和迁移学习是一个很好的方向&#xff0c;尤其是在军I领域。以下是一些你可以提前学习的基本 知识和步骤: 嵌入式系统:最近很多小伙伴找我&#xff0c;说想要一些嵌入式资料&#xff0c;然后我根据自己从业十年经验&#…

深入理解算法:从基础到实践

深入理解算法&#xff1a;从基础到实践 1. 算法的定义2. 算法的特性3. 算法的分类按解决问题的性质分类&#xff1a;按算法的设计思路分类&#xff1a; 4. 算法分析5. 算法示例a. 搜索算法示例&#xff1a;二分搜索b. 排序算法示例&#xff1a;快速排序c. 动态规划示例&#xf…

tcp专题

目录 一.TCP的连接建立 1.1面向连接 1.2TCP报文结构 1.3TCP三次握手 1.4TCP的状态变化 1.5为什么必须是三次握手&#xff0c;而不是两次或者四次 二.TCP的连接断开 2.1TCP的"四次挥手 2.2TCP的状态变化 2.3为什么要有TIME_WAIT状态 2.4为什么TIME_WAIT状态的时…

C++类和对象(三) (this指针)

this指针 1 this指针的引出 我们先来定义一个日期类 Date class Date { public:void Init(int year, int month, int day){_year year;_month month;_day day;}void Print(){cout << _year << "-" << _month << "-" << …

OpenSSL 密码库实现证书签发流程详解

目录 0. 基础理论openssl简介对称加密和非对称加密生成证书流程原理CA签发流程openssl基础操作 1. 生成证书的步骤与原理2. 标准的CA签发流程2.1 创建私钥&#xff08;.key)2.2 基于私钥创建证书签名请求&#xff08;.csr&#xff09;2.3 &#xff08;可选&#xff09;直接同时…

SystemVerilog Assertions应用指南 Chapter 1.14蕴含操作符

1.14蕴含操作符 属性p7有下列特别之处 (1)属性在每一个时钟上升沿寻找序列的有效开始。在这种情况下,它在每个时钟上升沿检查信号“a”是否为高。 (2)如果信号“a”在给定的任何时钟上升沿不为高,检验器将产生一个错误信息。这并不是一个有效的错误信息,因为我…

TCP通信-使用线程池优化

下面的通信架构存在问题&#xff1a; 客户端与服务端的线程模型是&#xff1a; N-N的关系&#xff0c;客户端并发越多&#xff0c;系统瘫痪的越快。 引入线程池处理多个客户端消息 代码实现 public class ClientDemo1 {public static void main(String[] args) {try {Syste…