我相信在大家学习递归算法时,第一个接触的就是斐波拉契数列问题吧。我这里重温下,斐波拉契数列问题:
存在这么一个数列:0、1、1、2、3、5、8、13、21、34; 其中f(0) = 0, f(1) = 1, 求f(n)
通过题目的描述,我们很容易发现特点:n >= 2时,当前元素等于前两个元素之和,即fib(n) = fib(n - 1) + fib(n - 2); 其中n >= 2,于是我们可以快速解题。
回到最初,斐波拉契数列的模样是这样子的:
0、1、1、2、3、5、8、13、21、34
从下(后)往上(前)分析推导出斐波拉契数列的递推关系式,并结合缓存实现动态规划
我们仔细观察后,很容易发现:从第n >= 2开始,当前位置的值是前两个元素的和。所以我们将数列的规则以表达式的方式定义出来:opt[n] = opt[n - 1] + opt[n - 2];其中n>=2,且opt[0] = 0; opt[1] = 1 有了这个后,我们很容易求出opt(n),因为opt(n)就是斐波拉契数列的递推关系表达式。但是,当我们求opt(5)时,发现还需要知道opt[4]和opt[3]的值才行(PS: 思考到这里就停止,不要继续去想opt[4]的计算需要opt[3]和opt[2],这样很容易把自己陷进去),那么能不能假设opt[4]和opt[3]的值已经存在了,直接拿结果(因为我们肯定知道opt[3]和opt[4]是要计算出来的,所以此时可以理解成计算出来就把它放入缓存中,此时在计算opt[5]时认为opt[3]和opt[4]已经存在了也是合理的)。如果是这样的话,那opt数组其实就是第三章中的hashMap缓存了。
于是,我们尝试解读下如下代码:
public static int dynamicFib(int n) { // 定义上述说的opt数组,长度为什么为n + 1呢? // 因为我们要求opt[5], 实际上它位于的是数组 // index为5的位置上,index == 5,那是不是说明 // 数组长度为6, 因此opt数组长度为n + 1 int opt[] = new int[n + 1]; // opt[0]和opt[1]的赋值为0和1,由题目可知 opt[0] = 0; opt[1] = 1; // 一层循环,i从2开始(满足上述的n >= 2时,才使用递推关系式进行求解) for (int i = 2; i <= n; i++) { // 递推关系式写在循环里面 // 当求opt[2]是,计算了opt[0]和opt[1] // 而opt[0]和opt[1]是已知条件 // 计算完后,就把值存在了opt中index = 2 // 的位置上了,完成了缓存的操作 opt[i] = opt[i - 1] + opt[i -2]; } // 数组中的最后一个元素就是要求的结果。 return opt[n]; }上述代码,就是使用动态规划的方式解决了斐波拉契数列的问题,时间复杂度为O(n),因为遍历了整个opt数组,只开启了一个长度为n的数组,所以空间复杂度为O(n)。至此,从空间复杂度和时间复杂度两个维度来考虑,使用动态规划的方式解决斐波拉契数列问题是最优的。
从斐波拉契数列的递归解法开始,到添加缓存优化时间复杂度,再到动态规划。要解决动态规划问题,若不能马上找到递推公式,可以从递归方法开始着手。
状态的定义: opt[n], dp[n], fib[n]所谓opt[n], dp[n], fib[n]表示的都是针对这个问题的递推公式,比如在斐波拉契数列的递推公式中,fib(n) = fib(n - 1) + fib(n - 2); 其中fib(n)表示的就是斐波拉契数列中的一种状态(求f(n)的状态)
状态转移方程:opt[n] = best_of(opt[n - 1], opt[n - 2], …)所谓状态转移方程,通常指的就是递推关系式,使用状态转移方程可以求解决最优子结构(最优子问题)
最优子结构最优子结构通常指的就是算法题中的一些最优解,比如一些常见的最值问题,通常都可以从状态转移方程中得到。