https://algorithms.tutorialhorizon.com/introduction-to-dynamic-programming-fibonacci-series/
什么是动态规划:
动态规划是一种更高效解决递归问题的技术. 在递归问题中我们总是重复的解决子问题,而在动态规划问题中,我们存储子问题的结果以便于我们不会再去重复的解决此问题,这种方式被称为 Memoization.
动态规划与Memoization协作,大多数的动态规划问题可由下面的两步解决:
- 递归 – 将问题分解为可以递归处理的子问题
- Memoization – 存储这些子问题的结果以便我们不需要去重复计算
例如:
Fibonacci 序列问题 : 当前数字的函数值是其前两个数字的函数值之和,定义如下:
Fibonacchi(N) = 0 for n=0 = 0 for n=1 = Fibonacchi(N-1)+Finacchi(N-2) for n>1
递归方案:
public int fibRecur(int x) {
if (x == 0)
return 0;
if (x == 1)
return 1;
int f = fibRecur(x - 1) + fibRecur(x - 2);
return f;
}
如图所示,要计算fib(4)需要计算fib(3)和fib(2), 计算fib(3)需要计算fib(2)和fib(1),你应该已经注意到在计算fib(4)时已经计算了fib(2), 而在计算fib(3)时依然要计算fib(2).所以我们是在一遍又一遍的计算子问题。
其时间复杂度:T(n) = T(n-1) + T(n-2) + 1 = 2n = O(2n)
使用Memoization:
存储子问题的结果以便于不需要重复计算。 在计算过程中首先要检查子问题的解是否已经存在,如果存在就使用它,不存在则去计算并存储下来为后面使用。
public int fibDP(int x) {
int fib[] = new int[x + 1];
fib[0] = 0;
fib[1] = 1;
for (int i = 2; i < x + 1; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib[x];
}
时间复杂度: O(n) , 空间复杂度 : O(n)
动态规划问题的两个主要特点-
我们可以通过下面的两个属性来确定一个问题是否可以应用动态规划来解决:
- 重叠子问题 (Overlapping Sub-problems)
- 最优子结构 (Optimal Substructure)
重叠子问题属性:
重叠子问题, 正如其名,其子问题需要被一遍又一遍的解决。在递归方案里,我们需要每次重新去解决这些子问题,而在动态规划方法里我们只需要解决一次这样的子问题并存储下来以备后用。就像我们下图看到的,我们在重复的解决这些子问题。
最优子结构: 如果一个问题可以由其子问题的方案来解决,我们称这种问题具有最优子结构属性。
动态规划方法:- 自底向上(Bottom-Up)
- 自上而下(Top-Down)
Bottom-Up 方法:
如果我们要解决输入为N的问题, 我们可以从解决最小的输入开始计算并存储结果为后面使用。接着可以计算稍大些的值并存储下来,以此类推.
Fibonacci 序列的自底向上方法:
public int fibDP(int x) {
int fib[] = new int[x + 1];
fib[0] = 0;
fib[1] = 1;
for (int i = 2; i < x + 1; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib[x];
}
Top-Down 方法:
将问题分解成子问题,根据需要解决子问题并存储结果为后面使用。
public int fibTopDown(int n) {
if(n==0) return 1;
if(n==1) return 1;
if(fib[n]!=0){
return fib[n];
}else{
fib[n] = fibTopDown(n-1) + fibTopDown(n-2);
return fib[n];
}
}