动态规划从0到1——从斐波那契数列开始入门

143 阅读3分钟

算法的基础掌握从递归开始,也就是《算法导论》中最初的分而治之思想。当然我们知道递归的效率是很低的,在很多情况下会出现指数级别的时间复杂度,因此学会将递归问题转化为迭代问题,是学习算法的下一个必经之路。其中有一大类问题就需要用到动态规划来解决。

对于一个优化问题来说,动态规划需要问题有以下两个特征:

  • 最优子结构
  • 子问题重叠行

我们从一到简单的斐波那契数列题目开始。

题目分析

来自兔群繁殖这一道题目。斐波那契数列又叫兔子数列,很容易想到,问题在于如何高效的求解这个问题。

递归版本

斐波那契求解方程如下:

F(n)={1,if n=1 or n=2,F(n1)+F(n2),if n>2.F(n)=\begin{cases}1,&\mathrm{if~}n=1\mathrm{~or~}n=2,\\F(n-1)+F(n-2),&\mathrm{if~}n>2.&\end{cases}

因此很容易写出递归版本的解答

def fib_recursive(n: int) -> int:
    if n == 1 or n == 2:
        return 1
    return fib_recursive(n - 1) + fib_recursive(n - 2)

然而这种方法存在明显的性能问题。每次计算时,都会重复计算相同的子问题。例如,计算 fib_recursive(5) 时会计算两次 fib_recursive(4) 和三次 fib_recursive(3),时间复杂度达到指数级别 O(2n)O(2^n)

动态规划的分析

动态规划的核心思想是自底向上解决问题,避免递归调用造成的额外栈空间开销。对于斐波那契数列,可以用一个数组 dp 来存储中间结果,其实就是提前存储好一张表,保存先前的计算信息,然后在后续解决问题时遇到先前的子问题就直接查表得到结果。

比如对于斐波那契数列,我们可以用一张简单的一维数组dp[i]来存储FiF_i,并初始化递归基,也就是dp[1] = 1; dp[2] = 1

def fib_dp(n: int) -> int:
    if n == 1 or n == 2:
        return 1
    dp = [0] * (n + 1)
    dp[1] = dp[2] = 1
    for i in range(3, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

复杂度分析

我们用到了一维数组,空间复杂度O(n)O(n),时间复杂度只需要填满这张表,因此是线性的。

进一步优化

实际上,我们只需要存储最近两个状态的值,而不需要整个数组。可以进一步优化为:

def fib_optimized(n: int) -> int:
    if n == 1 or n == 2:
        return 1
    prev2, prev1 = 1, 1
    for _ in range(3, n + 1):
        curr = prev1 + prev2
        prev2, prev1 = prev1, curr
    return curr

这样,时间复杂度仍然是线性的,但是空间复杂度降低成了常数。

总结

这个简单的斐波那契数列动态规划,已经阐释了子问题重叠性这一核心思想,在下面的系列中,将通过优化问题来进一步阐释优化子结构这一思想

使用豆包marscode AI的优势

在解决这类问题时,豆包marscode AI可以提供以下优势:

  • 代码生成:AI能够根据问题描述快速生成相应的代码框架,节省编写代码的时间。
  • 算法优化:AI能够分析代码并提供可能的优化建议,帮助提高算法的效率。
  • 错误检测:AI能够检测代码中的错误,并提供修改建议,确保代码的正确性。