reference:
【1】为什么你学不会递归?告别递归,谈谈我的一些经验
【2】告别动态规划,连刷40道动规算法题,我总结了动规的套路
【3】动态规划 无痛理解
个人认为,递归和动态规划,尤其能体现计算机编程的魅力。
【4】使用斐波那契数列引入了动态规划的概念
什么是斐波那契数列?
斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34….,即第一项 f(1) = 1,第二项 f(2) = 1……,第n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。
大家大概在小学的时候就接触过类似于这样的,找数学规律的题目。而在计算机编程中,这里的“规律”,就是我们编码的核心和基本。也叫做 「子问题」 或 「状态」。
例如上述的斐波那契数列,他的”规律“就是f(n) = f(n-1) + f(n-2)。
表示如下::
f
(
n
)
=
{
1
n<=2
f
(
n
−
1
)
+
f
(
n
−
2
)
n>2
f(n)= \begin{cases} 1 & \text{n<=2}\\ f(n-1) + f(n-2)& \text{n>2} \end{cases}
f(n)={1f(n−1)+f(n−2)n<=2n>2
递归是什么?
“递归”, 可以从字面意思理解,就是 「传递」 和 「回归」。所以递归最大的两要素就是 「传递规律」 和 「回归条件」。
例如,用递归来实现斐波那契数列。
一、先理解和处理函数程序的输入输出:输入是int n
, 输出是int 第n项的值
public int fiboseq(int n){
}
二、找到 「回归」 条件,也就是函数的结束条件。
public int fiboseq(int n){
// n减到什么时候不再做递归
if(n<=2){
return 1;
}
}
三、补充递归函数内核 「传递」 规律
public int fiboseq(int n){
// n减到什么时候不再做递归
if(n<=2){
return 1;
}
// n的传递规律
return fiboseq(n-1) + fiboseq(n-2);
}
我们可以亲切的把斐波那契数列问题看成鸡生蛋的问题。小鸡家族的每一只鸡都有一个使命,那就是自己生的蛋必须比前两只鸡生的蛋的总和。
现在我们是第n
只小鸡,我们不知道我们应该生多少只蛋,于是我去问前面两只鸡 n-1
和 n-2
return fiboseq(n-1) + fiboseq(n-2);
第n-1
只小鸡和第n-2
只小鸡说我也忘了我生了多少蛋了,我去问问我前面那两只鸡。
进入到
fiboseq(n-1) + fiboseq(n-2);
的内部:
fiboseq(n-1):
//第n-1只小鸡去问自己前面的第(n-1)-1只小鸡和第(n-1)-2只小鸡
return fiboseq((n-1)-1) + fiboseq((n-1)-2);
//第n-2只小鸡去问自己前面的第(n-2)-1只小鸡和第(n-2)-2只小鸡
fiboseq(n-2):
return fiboseq((n-2)-1) + fiboseq((n-2)-2);
直到找到了小鸡祖宗,也就是第1只和第2只鸡,
fiboseq(2):
public int fiboseq(int n){
//我就是第二只鸡啦,我生了一只鸡
if(n<=2){
return 1;
}
。。。
}
fiboseq(1):
public int fiboseq(int n=2){
//我就是第一只鸡啦,我生了一只鸡
if(n<=2){
return 1;
}
。。。
}
好的,那么现在第三只鸡知道自己生了1+1=2只鸡了,第四只鸡也知道自己生了1+2=3只鸡啦,第五只也知道。。。最后来到了第n只,也就知道了自己生了多少只鸡啦!
由此可见编程是多么的神奇,如此复杂的一层一层向下寻找,又最后一层一层的向上回传,用寥寥几行代码就可以表示。
什么时候会用到递归?
同时,我们也可以总结出,当数据向下层寻找的时候若规律相同,即可以无限地调用起自己所在的函数的时候,就可以用递归。
动态规划又是什么?
以n=8为例,上面的函数可以简化为下面的模型:
在这里插入图片描述
此时的1⃣️号🐔和2⃣️号🐔内心OS:你们这群鸡崽子能不能记点事儿啊?比如3⃣️号🐔,你来问自己生了多少蛋问了七次,牛教三次都知道打转呢!(此段责骂的reference是我的妈妈)。
但是这时候的3⃣️号🐔内心非常的委屈:可是人家只是一只鸡,记不住那么多事情嘛。所以,不如我把我生了多少只鸡写下来,方便四号查看,四号之后是五号。。。这样我就只用问两个鸡祖宗一次,就不会被骂啦!
于是乎,就有了 「动态规划」 ,与 「递归」 的 「自上而下」 不同, 「动态规划」 是 「自下而上的」。在函数里,我们用for循环,来实现n的向上传递。
public int fiboseq(int n){
// 两个鸡祖宗依然作为开山鼻祖
if(n<=2){
return 1;
}
int[] dp = new int[n+1];//设置一个缓存空间
//dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
// 从三号鸡开始往上递送消息
for(int m=3, m<=n; m++){
dp[m] = dp[m-1]+dp[m-2];
}
return dp[n];
}
对于一个递归结构的问题,如果我们在分析它的过程中,发现了它有很多“重叠子问题”,虽然并不影响结果的正确性,但是我们认为大量的重复计算是不环保,不简洁,不优雅,不高效的,因此,我们必须将“重叠子问题”进行优化,优化的方法就是“加入缓存”,“加入缓存”的一个学术上的叫法就是“记忆化搜索”。
另外,我们还发现,直接分析递归结构,是假设更小的子问题已经解决给出的实现,思考的路径是“自顶向下”。但有的时候,“自底向上”的思考路径往往更直接,这就是“动态规划”,我们是真正地解决了更小规模的问题,在处理更大规模的问题的时候,直接使用了更小规模问题的结果。
一维动态规划练习题
leetcode
#70 爬楼梯
#746. 使用最小花费爬楼梯
#198 打劫家舍
#413 等差数列划分