暴力递归如何转动态规划
这几天做了少量动态规划的题目,看题解的时候有个比较深的感触是很多答主一上来就抛出动态规划解法,着实让人摸不着头脑,每次看的时候都在想“这个转移方程究竟怎样推出来的”。所以博主结合最近看到的解答和牛客左神的课程以及自己的一点体会做个小结。
一. 明确什么是动态规划
动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者者说分治)的方式去处理,是暴力递归的优化版本。所以做算题遇到不能直接写出的动态规划时,从暴力递纳入手是个正确的选择,接下来我们看看两者的特点
暴力递归
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
动态规化
- 从暴力递归中来
- 将每一个子问题的解记录下来,避免重复计算
- 把暴力递归的过程,笼统成了状态表达
- 并且存在化简状态表达,使其更加简洁的可能
二. 解答流程
接着我们明确一般的解答流程:暴力递归解法->带记忆数组的递归解法->动态规划解法,只需按照这个流程去做基本都能解答出来。下面我以大家最为熟习的斐波那契数列入手,更多的例子可以看博主前面的LeetCode刷题之动态规划系列。 斐波那契数列的递推式是f(n)=f(n-1)+f(n-2)。(1,1,2,3,5,8…)
第一步:写出暴力递归解法
public int fibonacci(int n){ if (n == 1 || n == 2){ return 1; } return fibonacci(n - 1) + fibonacci(n - 2);}这个解法相信大部分人都能写出来,暴力递归之所以低效是由于存在大量的重复计算,借鉴LeetCode题解区一位大佬的图,如图所示,f(20)=f(19)+f(18),而f(19)=f(18)+f(17),这里就产生了重复计算,而且这种重复计算还很多,正是由于这些大量的重复计算,所以暴力递归很低效,这个算法的时间复杂度为 O(2^n)。

步骤二:带记忆数组的递归
步骤一的计算过程中国充斥着大量的重复计算,处理重复计算的方法很简单,用一个数组或者者其余容器装起来,递归的时候判断能否已经计算过的,假如已经计算过,就直接返回。这个是典型的用过空间换时间的做法,反应到上述递归图中就是“剪枝”了。(下图同样是借鉴的)

<figcaption></figcaption>
public int fibonacci(int n){ if (n < 1){ return 0; } int[] memo = new int[n + 1]; return helper(n, memo);}private int helper(int n, int[] memo){ if (n == 1 || n == 2){ return 1; } //假如计算过,直接返回 if (memo[n] != 0){ return memo[n]; } //没被计算过 memo[n] = helper(n - 1, memo) + helper(n - 2, memo); return memo[n];}步骤三:转为动态规划
写出来了带记忆数组的递归解法,动态规划也就基本成型了,由于这两者区别不是很大,前者是自顶向下的,后者是自底向上的。自顶向下的意思是,比方求f(5),递归的做法是先递归到f(1),而后再往上走得到f(5);而动态规划是直接从f(1)开始往上求的。
public int fibonacci(int n){ int[] dp = new int[n + 1]; dp[1] = 1; dp[2] = 1; for (int i = 3; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n];}总结
带记忆数组的递归和动态规划类似,他两的时间复杂度也相差无几,动态规划中很关键的转移方程就是从暴力递归中而来的,所以当遇到没做过或者者不能一下子写出转移方程的,从暴力递归做起总是一个正确的选择。
欢迎关注公众号:老男孩的成长之路,精选干货每周定期奉上!

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 暴力递归如何转动态规划