动态规划的核心思想就一句: 将问题拆分成更小的子问题并存储结果来避免重复计算。 也就是用空间换时间
从最经典的斐波那契数列来看动态规划:
斐波那契数列:从0和1开始,后续的每一项都是前两项的和
根据斐波那契数列的概念,直觉是可以通过递归得到计算结果
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
我们按步骤分析代码执行顺序:
调用 fibonacci(4)
→ fibonacci(3) + fibonacci(2)
调用 fibonacci(3)
→ fibonacci(2) + fibonacci(1)
调用 fibonacci(2)
→ fibonacci(1) + fibonacci(0)
fibonacci(1) 返回 1
fibonacci(0) 返回 0
所以 fibonacci(2) 返回 1
回到 fibonacci(3):
fibonacci(3) = fibonacci(2) + fibonacci(1)
= 1 + 1 = 2
回到 fibonacci(4):
现在计算 fibonacci(2)(这是之前的结果,我们需要再次计算):
→ fibonacci(1) + fibonacci(0)
fibonacci(1) 返回 1
fibonacci(0) 返回 0
所以 fibonacci(2) 返回 1。
最后,合并结果:
fibonacci(4) = fibonacci(3) + fibonacci(2)
= 2 + 1 = 3
因此,调用 fibonacci(4) 返回 3
该算法的时间复杂度为O(2^n),耗费的时间极长,我们分析代码可以发现,递归过程中,其中许多值(如 fibonacci(2))被多次计算,我们把计算过程产生的结果存储起来,不就可以提高我们算法的效率吗!
优化后的代码如下:
function fibonacci(n) {
if (n < 2) {
return n
}
const prev = [0, 1]
for (let i = 2; i <= n; i++) {
prev[0] = prev[1]
prev[1] = prev[0] + prev[1]
}
return prev[1]
}
我们只需要把数列的最后两个值存储起来,避免不必要的计算,就可以把时间复杂度降低到O(n),这个就是动态规划的核心思想。
但并不是所有问题都适合动态规划,判断一个问题是否适合使用动态规划解决,通常可以沿着以下几个步骤进行分析:
- 是否可以分解成子问题?子问题是否重复出现
- 最优解是否依赖于子问题的最优解
- 怎么定义问题的解决步骤
- 确定边界条件
- 分析问题的规模,以确保通过动态规划可以在合理的时间内解决它。