动态规划面试宝典
卢誉声
Autodesk 首席工程师
9629 人已学习
新⼈⾸单¥59
登录后,你可以任选2讲全文学习
课程目录
已完结/共 23 讲
动态规划面试宝典
15
15
1.0x
00:00/00:00
登录|注册

02 | 暴力递归:当贪心失效了怎么办?

你好,我是卢誉声。
上一课我们学习了贪心算法,提出了硬币找零的问题,发现了贪心算法的局限性。与此同时,我还提出了一个重要概念,那就是局部最优与整体最优的概念,即最优化问题。今天,我们就从最优化问题开始聊起,引出学习动态规划时的另一重要概念:递归。
我们之前说过,贪心算法是求解整体最优的真正思路源头,这是为什么我们要在这门课程的一开始从贪心算法讲起。现在,你应该已经意识到贪心算法是有局限性的,它只能在局部最优的思想下工作,那么当贪心算法失效了怎么办?
接下来我们就带着这个问题,开始学习今天的内容:递归!看看它能否更进一步地解决我们遇到的棘手问题,从整体最优的角度来解决算法问题。

从最优化问题到递归

贪心算法失效的很大一个原因在于它明显的局限性:它几乎只考虑局部最优解。所谓局部最优,就是只考虑当前的最大利益,既不向前多看一步,也不向后多看一步,导致每次都只用当前阶段的最优解。
因此在绝大多数情况下,贪心算法不能得到整体最优解,但它的解是最优解的一个很好近似。同时,也是所有讨论最优化问题的核心基础。
既然无法通过贪心算法达到整体最优,我们就得换一个思路了:我们得从整体最优的层面上解决这个难缠的算法问题。那么从何说起呢?我认为你应该先理解最优化问题的本质,然后再把这个思考扩展到递归问题上。话不多说,我们这就开始吧!
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

递归与动态规划在解决最优化问题中的重要性是本文的重点。文章深入探讨了贪心算法的局限性,并通过斐波那契数列和硬币找零问题引出了递归和动态规划的概念。递归被阐述为动态规划的基础,通过代码实现和理论分析,读者可以深入理解递归的概念和其在解决最优化问题中的应用。同时,文章指出了暴力递归的问题,包括性能低下、可读性差和调试困难等。为解决这些问题,提出了剪枝与优化的方法,包括利用预设条件减少搜索路径和避免重叠子问题的计算。最后,强调了递归在面试问题中的重要性,并展望了备忘录和动态规划的应用。整体而言,本文为读者提供了对动态规划和算法解决问题的新视角,是一篇值得深入阅读的技术文章。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《动态规划面试宝典》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(23)

  • 最新
  • 精选
  • 卢誉声
    置顶
    这里给出一种解决括号生成的解法,用C++语言描述,供大家参考: class Solution { public: vector<string> generateParenthesis(int n) { std::vector<string> v; backtrace("", 0, 0, n, v); return v; } void backtrace(std::string cur, int left, int right, int n, std::vector<string>& v) { if (left + right == 2*n) { v.push_back(cur); return; } if (left < n) backtrace(cur + "(", left+1, right, n, v); if (right < left) backtrace(cur + ")", left, right+1, n, v); } };
    2020-09-16
    2
    6
  • 小熊
    第二讲暴力递归 要得到整体最优解需要找出所有符合的情况,选出其中一个最优组合,符合整体最优。 全部跑出结果再判断,这会浪费很多空间来存储结果组合 所以在递归过程中,进入最内层返回得到结果时舍弃不符合的,保留更符合的,直到退回递归的顶层,就可以得到唯一的结果。 递归的方法是为了保存之前的状态,回退时使用,这其实就是栈的数据结构,可以改为非递归的方式。 使用递归,失败了可以回退,这种回退找正确答案的思路就是回溯,每次都找到树型结构的底部就叫深度优先的思路。 这种全部结果都遍历出来,取最优解的做法是暴力递归,性能差难以调试,效率低,可读性差,可以使用剪枝优化。 贪心算法是动态规划的源头,但是他局限于求局部最优解,但是用到暴力递归里,就可以达到剪枝的效果。 第二种优化策略,重叠子问题,余额相同的时候搜索路径是完全一致。所以可以把大问题拆分为小问题,这就是备忘录和动态规划的基础。

    作者回复: 理解没有问题。

    2021-01-20
    4
  • AshinInfo
    第一种思路是仿照贪心算法,从整个搜索策略上来调整。也就是说,你要考虑这个问题的性质,即面值大的硬币用得足够多,那么这个组合的硬币总数肯定就最小。 这句话我想了下,不是很同意这个说法。比如 硬币 10 7 1,金额 14。按照上面所说 那么他得到的解 10, 1, 1, 1, 1,这个时候就结束了。但其实最有解是7,7。老师您,上一课的代码只能获得一个解,并不能获取最优解

    作者回复: 上一课一开始是假定从大到小排列,这种情况只能获得这种假设下的局部最优解。但当我们后面求全组合之后(调整尝试硬币的顺序),就能得到全局最优解了。

    2020-10-27
    2
  • 宋不肥
    换了vector之后代码可读性好了好多,上一篇那个数组,看了好久。。。。。。

    作者回复: 哈哈,这也是循序渐进而刻意为之的。在真正面试中,我强烈推荐直接使用vector及其对应的工具函数,这在技术面试中是完全允许而且推荐的。

    2020-10-19
    2
  • HoSalt
    参照贪心算法的优化,起不到优化的作用吧,前面就有评论也说过,需要对比所有的结果才知道是不是最优,除非只是想得到一个结果,而这个结果要尽可能的好。 或者是基于前面得到的解作为其它递归的剪枝条件之一

    作者回复: 如果要用贪心达到优化的作用,往往是利用贪心策略进行剪枝。在本问题中,主要是为了说明在有些情况下,贪心本身并不能直接解决问题。

    2020-09-19
    2
  • 飞跃疯人院
    JavaScript: ``` var generateParenthesis = function (n) { const ret = [] const _helper = (left, right, parenthesi) => { if (left === 0 && right === 0) { ret.push(parenthesi) } if (left > 0) { _helper(left - 1, right, parenthesi + '(') } if (right > left) { _helper(left, right - 1, parenthesi + ')') } } _helper(n, n, '') return ret }; ```

    作者回复: 这个递归的代码思路没问题。

    2020-12-09
    1
  • 憨才好运
    这里要说明一下Java和C++的区别: // List<Integer> initialCounts = new ArrayList<>(2); // Collections.fill(initialCounts, 0); List<Integer> initialCounts = Arrays.stream(new int[values.length]) .boxed().collect(Collectors.toList()); 初始化之后的initialCounts依然是0,直接访问修改会发生越界错误的,所以采用lamda的方式初始化initialCounts You're confusing the size of the array list with its capacity: the size is the number of elements in the list; the capacity is how many elements the list can potentially accommodate without reallocating its internal structures. When you call new ArrayList<Integer>(10), you are setting the list's initial capacity, not its size. In other words, when constructed in this manner, the array list starts its life empty. One way to add ten elements to the array list is by using a loop: initialCounts.add(0);

    作者回复: 感谢你指出,这里使用了 ArrayList<Integer> initialCounts = new ArrayList<>(Collections.nCopies(values.length, 0)); 修正了问题。

    2020-09-18
    1
  • webmin
    ```java public List<String> genParenthesis(int n) { List<String> res = new ArrayList<>(); dfs(0,0,n,"",res); return res; } private void dfs(int l,int r,int n,String s,List<String> res){ if(l == r && l == n){ if(check(s)){ res.add(s); } return; } if(l < n){ dfs(l+1,r,n,s + "(",res); } if(r < n){ dfs(l,r+1,n,s + ")",res); } } private boolean check(String s){ Stack<Character> stack = new Stack<>(); for(char c : s.toCharArray()){ if(c == '(') { stack.push(c); continue; } if(stack.isEmpty()){ return false; } stack.pop(); } return stack.isEmpty(); } ```

    作者回复: 这个解法是可以工作的,代码中通过深度优先(DFS) 生成括号,再通过check 函数来监测括号组合是否有效。但其实更进一步的利用回溯来简化执行流程,以及降低执行时间。

    2020-09-16
    2
    1
  • Monroe He
    # 暴力递归 python实现 # 枚举出满足条件的局部最优解,进而求得全局最优解 # 枚举的方法就是递归 # 问题描述:硬币组合,给出硬币面值,计算特定值的最小硬币数量 # 如:面值:[3, 5] 特定值:11 返回:3 import sys def get_min_counts_helper(total, values): # 如果余额为 0,说明当前组合成立,将组合加入到待选数组中 if total == 0: return 0 # 面值个数 value_length = len(values) # 返回最小硬币数量 min_count = sys.maxsize for i in range(value_length): # 如果当前面值大于硬币总额,那么跳过 if values[i] > total: continue # 特定值减去当前面币值后余额 rest = total - values[i] rest_count = get_min_counts_helper(rest, values) # 如果返回 -1, 说明组合不可信,跳过 if rest_count == -1: continue # 硬币数量增加一枚 total_count = rest_count + 1 # 保留最小总额 min_count = total_count if total_count < min_count else min_count # 如果没有可用组合,返回 -1 if min_count == sys.maxsize: return -1 return min_count def get_min_count_of_coins(): values = [3, 5] # 硬币面值数组 total = 11 # 特定值 return get_min_counts_helper(total, values) get_min_count_of_coins()

    作者回复: 挺棒的,实现没有问题,赞

    2022-11-08归属地:北京
  • 阳仔
    关键代码解释得不是很清楚,看文本都懂,看这个代码真的懵了

    作者回复: 代码中已经有了详细的备注,建议可以自己写一下代码,Practice makes it perfect,去可以理解这里的示例代码了。

    2021-09-29
收起评论
显示
设置
留言
23
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部