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

news2024/11/14 14:18:43

参考引用

  • 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 阶时,方案数量加 1
        if (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++;
    }
    // 若未找到可行方案,则返回 -1
    return 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.coloradmin.cn/o/1116775.html

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

相关文章

Django和jQuery,实现Ajax表格数据分页展示

1.需求描述 当存在重新请求接口才能返回数据的功能时&#xff0c;若页面的内容很长&#xff0c;每次点击一个功能&#xff0c;页面又回到了顶部&#xff0c;对于用户的体验感不太友好&#xff0c;我们希望当用户点击这类的功能时&#xff0c;能直接加载到数据&#xff0c;请求…

C#,数值计算——分类与推理Phylagglomnode的计算方法与源程序

1 文本格式 using System; using System.Collections.Generic; namespace Legalsoft.Truffer { public class Phylagglomnode { public int mo { get; set; } public int ldau { get; set; } public int rdau { get; set; } public …

# 用acme.sh申请证书(含泛域名)

用acme.sh申请证书&#xff08;含泛域名&#xff09; 文章目录 用acme.sh申请证书&#xff08;含泛域名&#xff09;1 申请证书&#xff1a;1.1 使用dns api方式申请证书&#xff08;以阿里云dns为例&#xff09;1.2 附加&#xff1a;也可以用其他方式申请证书 2 续签证书&…

跨域方案的抉择

前言 遇到跨域问题的时候&#xff0c;到底是使用CORS来解决&#xff0c;还是使用代理呢&#xff1f; 判断依据不是技术层面&#xff0c;而是你的生产环境。 首先要关注的是生产环境里面到底是一种什么样的情况&#xff0c;到底有没有跨域&#xff0c;然后根据生产环境的情况&a…

yjs demo: 多人在线协作画板

基于 yjs 实现实时在线多人协作的绘画功能 支持多客户端实时共享编辑自动同步&#xff0c;离线支持自动合并&#xff0c;自动冲突处理 1. 客户端代码&#xff08;基于Vue3&#xff09; 实现绘画功能 <template><div style"{width: 100vw; height: 100vh; over…

Matlab遗传算法工具箱——一个例子搞懂遗传算法

解决问题 我们一般使用遗传算法是用来处理最优解问题的&#xff0c;下面是一个最优解问题的例子 打开遗传算法工具箱 ①在Matlab界面找到应用程序选项&#xff0c;点击应用程序(英文版的Matlab可以点击App选项) ②找到Optimization工具箱&#xff0c;点击打开 创建所需要…

[SQL开发笔记]INSERT INTO 语句:将新记录插入到数据库表中

目前&#xff0c;向数据库插入数据是数据管理的重要环节&#xff0c;它可以将数据长期保存、共享访问、保证数据的完整性和安全性&#xff0c;同时也是进行数据检索和分析的基础。其中&#xff0c;INSERT INTO 语句是SQL&#xff08;结构化查询语言&#xff09;中用于向数据库表…

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

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

代码随想录二刷 Day 44

01背包问题二维做法先遍历背包或者物品都可以&#xff0c;然后是前序遍历&#xff1b; 一维做法一定先遍历物品然后遍历背包&#xff0c;遍历背包的时候是后序遍历&#xff1b;一维做法还是有点难理解&#xff0c;其实就是后面的数字还是要从前面的推导出来&#xff0c;但是如…

全球3小时气象数据集GLDAS Noah Land Surface Model L4 3 hourly 0.25 x 0.25 degree V2.1

简介 全球3小时气象数据集&#xff08;GLDAS Noah Land Surface Model L4 3 hourly 0.25 x 0.25 degree V2.1&#xff0c;简称GLDAS_NOAH025_3H 2.1&#xff09;&#xff0c;时空分辨率分别为3小时、0.25度。该数据产品于2020年1月重新处理&#xff0c;代替之前版本。前言 – …

RunnerGo 支持UI自动化的测试平台

RunnerGo提供从API管理到API性能再到可视化的API自动化、UI自动化测试功能模块&#xff0c;覆盖了整个产品测试周期。 RunnerGo UI自动化基于Selenium浏览器自动化方案构建&#xff0c;内嵌高度可复用的测试脚本&#xff0c;测试团队无需复杂的代码编写即可开展低代码的自动化…

基于侏儒猫鼬优化的BP神经网络(分类应用) - 附代码

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

Oracle监听服务启动后停止

问题 解决办法 找到listener.ora文件,箭头指的地方&#xff0c;host改为localhost 如何找到listener.ora 其中1522端口&#xff0c;是我新增的监听服务。之前这个host是一个固定的ip地址&#xff0c;我更换网络环境后&#xff0c;ip地址变了&#xff0c;所以导致监听启动失败。…

基于白鲸优化的BP神经网络(分类应用) - 附代码

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

【Python Numpy教程】numpy数据类型

文章目录 前言一、安装numpy包二、numpy的数据类型2.1 NumPy数据类型概述类型类型字符代码 三、创建数据类型对象3.1 numpy.dtype介绍3.2 示例代码&#xff1a; 总结 前言 NumPy是Python中最常用的科学计算库之一&#xff0c;它提供了高性能的多维数组对象&#xff08;ndarray…

CSS 滚动驱动动画 timeline-scope

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

BAT034:批处理打开电脑常用功能面板

引言&#xff1a;编写批处理程序&#xff0c;输入相应功能序号&#xff0c;实现打开打开百度搜索、启动磁盘清理、启动注册表编辑器、启动系统配置、启动控制面板、启动画图程序、启动计算器程序、启动DirectX诊断工具、启动服务、启动计算机管理、启动系统信息、启动更改适配器…

基于法医调查优化的BP神经网络(分类应用) - 附代码

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

基于食肉植物优化的BP神经网络(分类应用) - 附代码

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

《向量数据库指南》——向量数据库Milvus Cloud快速打造知识库 AI 应用

快速打造知识库 AI 应用 具备知识库的 AI Chatbot 已然是当下基于大模型技术实现及应用最多的情景,接下来,我们将以制作一个具备 Dify 产品及团队知识背景的 AI 应用为例,为大家介绍如何从零开始,用 3 步搭建一个具备企业知识库的 AI 应用。 平台注册 在本次实操演示中,我…