解决递归中的重复计算问题

深入理解递归与动态规划在问题解决中的应用 本文将探讨如何通过递归和动态规划解决编程问题,特别是八皇后问题和箱子堆叠问题。文中详细描述了解决方案的实现,并且强调了递归解决问题时子问题之间的关系。此外,还讨论了布尔表达式的求值问题,包括如何使用递归和备忘录法来优化计算。通过对这些问题的分析,文章揭示了递归和动态规划在实际编程中的强大能力以及优化策略的重要性。 阅读详情

一、重叠子问题

斐波那契数列没有求最值的问题,因此严格来说它不是最优解问题,当然也就不是动态规划问题。但它能帮助你理解什么是重叠子问题。首先,它的数学形式即递归表达是这样的:
在这里插入图片描述

def Fibonacci(n):
    if (n == 0 or n == 1):
        return n
    if (n > 1):
        return Fibonacci(n - 1) + Fibonacci(n - 2)

def main():
    result = Fibonacci(4)
    print(result)

if __name__ == "__main__":
    main()

这个代码本身没有问题, 但是它的效率极低。假设上面的函数调用输入是10,把递归树画出来:
在这里插入图片描述
如果要计算原问题F(10), 就需要先计算出问题F(9)和F(8), 如果要计算F(9), 就需要先计算出子问题F(8) 和 F(7),以此类推。这个递归的终止条件是当F(1)=1 或 F(0)=0时结束。

看完斐波那契数列的求解树之后,就会发现问题:

  • 用红色标出的两个区域中,它们的递归计算过程完全相同

这意味着,第2个红色区域的计算是 “完全没有必要的”,它是重复的计算。因为我们已经在求解F(7)的时候把F(6)的所有情况算过了。因此我们把第2个红色区域的计算称为重叠子问题。

二、使用备忘录解决重复问题

既然存在重复的子问题,那我们在遇到这些重复的子问题时,只需要执行一次即可,即消灭重复计算的过程。

我们可以创建一个备忘录(memorization),在每次计算出某个子问题的答案后,将这个临时的中间结果记录到备忘录里,然后再返回。

接着,每当遇到一个子问题时,我们不是按照原有的思路开始对子问题进行递归求解,而是先去这个备忘录中查询一下。如果发现之前已经解决过这个子问题了,那么就直接把答案取出来复用,没有必要再递归下去耗时的计算了。

对于备忘录,可以考虑使用以下两种数据结构:

  • 数组(Array),通常对于简单的问题来说,使用一维数组就足够了。
  • 哈希表(Hash table),如果你存储的状态不能直接通过索引找到需要的值(比如斐波那契数列问题,你可以直接通过数组的索引确定其对应子问题的解是否存在,如果存在你就拿出来直接使用),比如你使用了更高级的数据结构而非简单的数字索引,那么你还可以考虑使用哈希表,即字典来存储中间状态,来避免重复计算的问题。

下面看看用数组实现的备忘录来解决斐波那契数列的代码:

def Fibonacci(n, memo):
    if (n == 0 or n == 1):
        return n
    # 如果备忘录中找到了之前计算的结果,那就直接返回,避免重复计算
    if (memo[n] != None):
        return memo[n]
    if (n > 1):
        memo[n] = Fibonacci(n - 1, memo) + Fibonacci(n - 2, memo)
        return memo[n]
    # 如果数值无效(比如 < 0),则返回0
    return 0

def FibonacciAdvance(n):
    memo = [None] * (n + 1)
    return Fibonacci(n, memo)

def main():
    result = FibonacciAdvance(4)
    print(result)

if __name__ == "__main__":
    main()

实际上,这就是我们所熟知的“剪枝与优化”,把一棵存在巨量冗余的递归树通过剪枝,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。通过这种方式,我们大幅缩减了算法的计算量,因为所有重复的部分都被跳过了

三、重叠子问题的限制

有些问题虽然看起来像包含“重叠子问题”的子问题,但是这类子问题可能具有后效性,但我们追求的是无后效性。
所谓无后效性,指的是在通过 A 阶段的子问题推导 B 阶段的子问题的时候,我们不需要回过头去再根据 B 阶段的子问题重新推导 A 阶段的子问题,即子问题之间的依赖是单向性的。
换句话说,如果一个问题可以通过重叠子问题缓存进行优化,那么它肯定都能被画成一棵树。

四、方法的弊端

通过重叠子问题缓存可以极大加速我们的代码执行效率。但是凡事都有两面性,毋庸置疑,这种方案肯定是通过某种牺牲换取了性能的提升。
在硬币找零问题中,我们就可以利用备忘录来避免重复计算。但这样有个问题,如果我们的钱币总额数量非常巨大,那这个数组的大小就会非常巨大,导致的结果就是会占据大量的内存存储空间,而且有很多的数字其实是不会被求解的,存在很多的“存储空洞”。显然,这是一种浪费。
所以在解题的过程中,你需要根据实际情况,在空间和时间中寻求一个平衡,将这个问题考虑在内。

五、总结与升华

备忘录的思想极为重要,特别是当求解的问题包含重叠子问题时,只要问题包含重复计算,你就应该考虑使用备忘录来对算法时间复杂度进行简化。具体来说,备忘录解法可以归纳为:
1.用数组或哈希表来缓存已解的子问题答案,并使用自顶向下的递归顺序递归数据;
2.基于递归实现,与暴力递归的区别在于备忘录为每个求解过的子问题建立了备忘录(缓存);
3.为每个子问题的初始记录存入一个特殊的值,表示该子问题尚未求解;
4.在求解过程中,从备忘录中查询。如果未找到或是特殊值,表示未求解;否则取出该子问题的答案,直接返回。

朴素的动态规划,使用一维数组改进消除递归重复计算,消除递归这三种方法.cpp 朴素的动态规划,使用一维数组改进消除递归重复计算,消除递归这三种方法解决01背包问题,通过数据比较和运行的结果上来看,消除递归重复计算的递归算法效率相当高,在6组数据的时候,将效率提高了几乎一倍,我使用的消除递归算法的结果和普通动态规划的效率差不多。 立即下载

相关推荐

SpringBoot 指标监控——Actuator

1、 访问:localhost:8000/actuator 设置健康监控显示具体情况 metrics http://localhost:8000/actuator/metrics/jvm.gc.live.data.size 定制化health信息 //利用构造器,在注册中心中添加cityService.saveCity.count指标 public CityServiceImpl(MeterRegistry meter...

weixin_44302046的博客 4630

如何解决递归中的重复计算(重叠子问题)

递归是一种自顶向下的、很直观的编程方法,解释性强。但在写递归的时候,最关键的问题是怎样避免递归过程中可能产生的大量重复计算,即重叠子问题,否则,写出来的代码即便没有逻辑错误,也几乎会因为超时无法运行。所以说,一旦选择了递归,基本上都要连同做后续的优化工作,这也是本文主要探讨的话题。 由于递归代码是很容易写的,这篇文章要做的是在写好暴力递归之后,怎样进行优化,以避免重复计算。下面通过几个简单的例子说明(题目来自力扣): 提示:不要关注题目内容,直接对比暴力递归代码和优化后代码,很容易找到优化套路,就在暴力基础

ccJun-的博客 3125

iOS自动化之打包-xcodebuild命令行打包.app并安装到ios模拟器

xcode xcodebuild xcrun ios模拟器 iOS UI自动化测试 ios自动打包安装到ios模拟器

z12347891的博客 5074

解决递归中的重复计算问题——保存临时的中间结果

避免在递归中重复计算前情提要重叠子问题使用备忘录解决重复计算重叠子问题处理模式重叠子问题的限制方案弊端总结与升华 前情提要 在上一篇文章(从贪心算法到暴力递归法——从局部最优到整体最优),我们提到了最优解问题,在考虑整体最优的情况下,需要找到一种办法获取最优解。 那么最简单直接的做法其实就是把所有可行的答案穷举出来,然后在所有可行的答案中找出满足条件的最值。这样的解法看似“天衣无缝”,但它有着重要的缺陷——执行效率极低。 导致这个问题的罪魁祸首是重叠子问题,那么应该如何解决重叠子问题并提高算法效率呢? 重叠

zbp_12138的博客 2925

知识图谱-构建:知识图谱构建流程【本体构建、知识抽取(实体抽取、 关系抽取、属性抽取)、知识表示、知识融合、知识存储】

旅游领域的类是用来描述旅游的抽象化概念术语,是对众多旅游个体共性的概念化描述。术语是划分类的基础,根据构建领域本体的目标和具体需要,可以确定哪些术语能作为领域本体的类。在以上抽取的众多旅游领域术语中,有些是能直接作为类的,如旅游、人物、交通方式、景区等等,而有些则不能作为类,如票价、景区门票、联系方式等等,它们只是属性。“旅游”很明显是最顶层的类,是父类。然后,要确定旅游的子类,根据大众标签里体现的共性和《中国分类主题词表》确定了旅游领域的11个核心概念,即本体层次结构图数据属性(固有属性)...

u013250861的博客 4万+

手把手教你ensp仿真搭建校园网(含防火墙及无线网络)系列(三)校园无线网络AC+AP配置

本系列博客将带你从零开始,使用 华为 eNSP(Enterprise Network Simulation Platform) 仿真搭建一个完整的校园网络,涵盖 核心层、汇聚层、接入层、防火墙安全策略、无线AC+AP组网 等实战场景。无论你是网络初学者还是希望深化实战经验的工程师,都能通过本教程掌握企业级网络的规划、配置与排错技巧。

echo的博客 1359

03 | 备忘录:如何避免递归中的重复计算?

斐波那契数列没有求最值的问题,因此严格来说它不是最优解问题,当然也就不是动态规划问题。但它能帮助你理解什么是重叠子问题。首先,它的数学形式即递归表达是这样的:return 0;// 如果输入n有误,则返回默认值这个代码有问题吗?没问题,但是它效率极低。那么它效率低在哪里呢?假设上面的函数调用输入是 10,把递归树画出来:我们要怎么理解这个递归树呢?

qq_37756660的博客 1417

【DRAM存储器五十一】LPDDR5介绍--DFE与Pre-Emphasis

简述LPDDR5的DFE和预加重应用设置

highman110的博客 706

递归优化的这三种方式你知道吗?

估计找工作的,都会碰到面试官老是问道“递归算法”,感同身受,前段时间面试的时候,就有一家问道这个问题,是非常典型的问题。递归应该算是比较“经典”的算法。

寒冰屋的专栏 1109

【零基础入门unity游戏开发——3D篇】3D物理关节 —— Joint相关组件

【零基础入门unity游戏开发——3D篇】3D物理关节 —— Joint相关组件

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 1202

rtpengine 项目

rtpengine 项目

原来45的博客 1789

如何解决递归重复计算问题

        在日常编写递归函数的时候,存在一个问题,即重复计算(如下图所示)。解决该问题的常用方法之一是将已经计算过的节点值用一个map容器保存起来,每次递归先查找该容器中是否存在该节点(key),如果存在则直接返回对应的值(value);否则计算,并将新的节点(key)和值(value)存入map中,具体参考实现代码以及注释,比较清楚  C++实现: #include&lt;ios...

成天天的博客 3102

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

递归实现有哪些常见的问题?

问题描述:递归必须有一个明确的基准情况(base case),用于停止递归。如果没有正确地定义基准情况,或者基准情况永远无法达到,那么递归将会无限循环下去,最终导致栈溢出或其他异常。解决方案:确保基准情况存在:设计递归算法时,务必保证至少有一种情况可以直接返回结果而不再进一步递归。验证输入合法性:对于递归函数的输入参数进行校验,防止非法输入导致基准情况失效。

邮箱:uemgyth@163.com 902

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

数据结构与算法之美 | 递归(Recursion)

什么叫做递归?

是希望的博客 951

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

数的计算 (递归)

洛谷新手村的题,感觉自己简直菜的一批,起初老实递归做TLE了,仔细读才发现还有一丝动态规划的味道。。。。 附代码: #include <iostream> using namespace std; int A[1000]; int digui(int a){ int k=1; for(int i=1;i<=a/2;i++){ if(A[i]!=0)...

chineseherofeng的博客 938

重新认识动态规划以及递归

A good solution is to keep track of values that have already been computed by storing them in a dictionary!为什么要把动态规划与递归这两个概念拿出来说,因为这里面我认为是有想通的地方的。而且还有很多我们编程中需要掌握的一些法则。新罗列一下会提及的原则, 1、DRY原则(不要重复自己!)

知道者 3223

Python递归优化避免重复递归:递归程序设计艺术(6)

如果不加以优化的话,递归很容易出现重复计算的问题。比如前面计算斐波那契数列,根据公式有F(n)= F(n-1)+F(n-2)。这意味着为了计算F(8),必须计算F(7)和F(6)。而为了计算F(7),必须计算F(6)和F(5),......。这里F(6)就被计算了两次。一般地,递归程序越靠近边界,重复计算的次数就会呈指数增加。当求F(36)时,电脑已经完全僵死,没有反应了。 那怎么解决这个问题呢?规范的方法是这样的: 要保证递归程序除了参数之外没有读写任何外部数据。这是为了保证程序的优化只在程序内...

方老师的博客 956

每日一题12:用数组加速递归

许多教学书上都用斐波那契数列(数列中第一二个值都是1,其它任意一个数都是其前两项之和)作为讲解递归的例子,作为教学例子,它确实十分合适,但是如果用在实际计算中,那么递归实现的斐波那契数列求值实在是太满了,其中主要的原因是重复计算实在太多,这样的递归算法不仅速度效率低下,还容易造成栈溢出。如果能够保留下已经计算过的值,但需要时直接取用而不是重复计算,那么必然会提高程序性能。 对于斐波那契数列求解使

爱拍球的程序圆 772

递归---从台阶问题学习递归、递归优化和非递归

递归就是将大问题划分为若干个子问题,各个问题是嵌套关系,最小的那个问题的结果是已知的,大问题不断分解直到达到最小问题的过程叫做“递”,小问题的解释已知的,然后根据这个解回过去求大问题的解的过程叫做“归”。 最简单的递归的例子就是求n的阶乘: 其递推公式为:f(n) = n * f(n-1),其中,f(1) = 1 递归需要满足三个条件 一个问题的解可以分解为几个子问题 这个问题与分解之后的子...

IT_10的博客 478

半数集问题

«问题描述: 给定一个自然数n,由n 开始可以依次产生半数集set(n)中的数如下。 (1) n∈set(n); (2) 在n 的左边加上一个自然数,但该自然数不能超过最近添加的数的一半; (3) 按此规则进行处理,直到不能再添加自然数为止。 例如,set(6)={6,16,26,126,36,136}。半数集set(6)中有6 个元素。 注意半数集是多重集。 «算法设计: 对于给

Neil的博客 5289

递归算法在计算机科学中是指一种,「递归算法」看这一篇就够了|多图

前言递归是一种非常重要的算法思想,无论你是前端开发,还是后端开发,都需要掌握它。在日常工作中,统计文件夹大小,解析xml文件等等,都需要用到递归算法。它太基础太重要了,这也是为什么面试的时候,面试官经常让我们手写递归算法。本文呢,将跟大家一起学习递归算法~什么是递归?递归的特点递归与栈的关系递归应用场景递归解题思路leetcode案例分析递归可能存在的问题以及解决方案什么是递归?递归,在计算机科学...

weixin_39768388的博客 1104

从递归到递推--入门篇

从递归到递推--入门篇

2401_86909484的博客 1227

算法:暴力解决法

@[TOC]算法实战总结——暴力解决法 #算法实战总结——暴力解决法 在家里准备竞赛的同时也想把自己最近学习到的东西每次学习进行一个总结,新人初学算法,有不对的地方希望各位大佬们能够指正。 理论背景:递归调用和穷举搜索法 1递归调用: 我自己对递归的理解就是不断地调用自身先由上而下递进,一般问题可能直接找到问题的出口就能够解决,但是像很多计算问题解得个数的问题就需要再向上回溯,是否能够运用递归的方...

weixin_44313114的博客 2395

数据结构与算法专题汇总(四)递归,台阶问题,递归防止栈溢出,利用散列表避免重复计算,

1.递归条件 一个问题的解可以分为子问题的解 分解后的子问题,与原问题的解题思路一致,数据规模不一致 存在递归终止条件 写出递归公式和终止条件!!! 写出递归公式和终止条件!!! 写出递归公式和终止条件!!! 2.举例 台阶问题:假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?如果有 7 个台阶,你可以 2,2,2,1 这样子上去,也可以 1,2,1,1,2 这样子上去,总之走法有很多,那如何用编程求得总共有多少种走法呢? 解答: 可以根据第一步的走法

April_er的博客 402

重生之我在异世界学编程之C语言:深入函数递归篇

递归(Recursion)是计算机科学中的一个重要概念,它指的是一个函数直接或间接地调用自身的方法。那现在宝子们就跟着小编的步伐一起进入本章知识的学习。Go!Go!Go!

z15879084549的博客 2431
上一篇: python 接雨水
下一篇: python 斐波那契数列
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一叶知秋的BLOG

创作不易 请多多关注

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值