Outline
Fibonacci数列问题
最优子结构和递推表达式
Fib问题的各类变种
青蛙跳-台阶跳
硬币找零问题(敬请期待)
一、Fibonacci数列
1.1 最优子结构和递推表达式
在很多生活场景中,我们都会遇到求解F(n) = F(n-1) + F(n-2)这类问题。它是一类非常重要的经典的简单的动态规划问题。首先F(n)的解是由F(n-1)和F(n-2)组成,而F(n-1)的解是由F(n-1-1),F(n-1-2)组成,因此求解该类问题的过程就是不断寻找F(n-x)的解的过程,因此F(n-x)都F(n)的子问题。具备子问题的问题都可以大化小,小化0与1,大大降低问题复杂度。简单来说,我们把这种具备子问题的特征叫做最优子结构性质。
找到子问题后,求解子问题,显然这个问题中子问题最终会转化为F(1), F(0),规定F(0) = 1;F(1) = 1,因此子问题F(2)= 1+1可以轻松求解。获取到子问题的解是不够的,我们最终的目的是得到原问题的解,因此需要找到子问题和原问题之间的关系,在Fibonacci问题中轻松获取两者关系F(n) = F(n-1) + F(n-2)。我们把两者的关系叫做递推表达式。
有了子问题和递推表达式,那么求解原问题就迎刃而解了。
回到Fibonacci问题,注意到F(n-1) = F(n -2) + F(n-3) ,F(n-2)即用于求解F(n)也用于求解F(n-1)那么我们将第一次求解F(n-2)获取的值保存起来,那么就不必重复计算,加快效率。因此我们将cumbersomeO(2^n)的算法改进为O(n)的算法。
//cumbersome version
long long cumbFib(int n)
{
if (n == 0)
{
return 1;
}
if (n == 1)
{
return 1;
}
return cumbFib(n-1) + cumbFib (n-2);
}
// advanced version
long long modFib(int n,vector<long long> memo)
{
if (memo[n] != 0)
{
return memo[n];
}
memo[n] = modFib(n-1,memo) + modFib(n-2,memo);
return memo[n];
}
// another advance version -非递归方法
long long modFib2(int n, vector<long long> memo)
{
memo[1] = 1;
memo[0] = 1;
for(int i = 2 ; i <= n; i++)
{
memo[i] = memo [i-1] + memo [i-2];
}
return memo[n];
}
利用数组将每次计算得到的数据先保存起来,用空间换时间。然鹅实际上,我们还可以继续优化。由于没次计算只需要用到两个旧数据,那么我们直接用变量保存这两个旧数据就ok啦。
long long smartFib(int n){
long long pre = 1,ppre = 1,cur ;
for (int i = 2;i <= n; i ++)
{
cur = pre + ppre;
pre = ppre;
ppre = cur;
}
return cur;
}
好啦,写到这里,我想应该不能再继续优化了吧!(嗯哼,any question?)那么接下来讨论此类问题的具体应用场景。
1.2 具体应用
1.2.1 青蛙跳问题
这个题目是经典的Fibonacci数列问题,甚至没有一点改动。值得注意的一点是Note:0 <= n <= 100,如果没有取模,那就必须考虑大数越界问题了。测试发现,当n >=92 时,C++ 中8字节long int已经无法表示其结果啦。n>=95时,最大范围的Unsigned long long也扑街了,C++数据类型范围表如下表所示(参考C官网文件)。至于Python则不需要考虑,因为python中整形数字的大小取决计算机的内存 ,暂且无需考虑大数越界问题。
Reference
https://en.cppreference.com/w/c/types/limits