【算法五】从斐波那契到动态规划:自顶向下、自底向上与状态压缩

一、问题:斐波那契数列

斐波那契数列(Fibonacci Sequence)是计算机科学中最经典的入门问题。它的数学定义如下:

  • F(0)=0F(0) = 0F(0)=0
  • F(1)=1F(1) = 1F(1)=1
  • F(2)=1F(2) = 1F(2)=1
  • 当 n>2n > 2n>2 时,F(n)=F(n−1)+F(n−2)F(n) = F(n-1) + F(n-2)F(n)=F(n−1)+F(n−2)

展开前几项:

复制代码
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...

相邻两项的比值 F(n+1)F(n)\frac{F(n+1)}{F(n)}F(n)F(n+1) 会无限趋近于黄金分割比 ϕ≈1.618\phi \approx 1.618ϕ≈1.618。但在算法领域,斐波那契数列真正的价值不在于数列本身,而在于它完美展示了"重叠子问题"和"动态规划"的核心思想,是理解 DP 最理想的起点。


二、暴力递归:最直觉但最慢的解法

按照数学定义,最直觉的写法就是直接翻译成递归函数:

java 复制代码
public static int fib(int N) {
    if (N == 0) return 0;
    if (N == 1 || N == 2) return 1;
    return fib(N - 1) + fib(N - 2);
}

递归的终止条件

java 复制代码
if (N == 0) return 0;
if (N == 1 || N == 2) return 1;

这三行叫递归的"出口"或"边界条件" 。斐波那契数列的 base case 是 F(0)=0F(0)=0F(0)=0、F(1)=1F(1)=1F(1)=1、F(2)=1F(2)=1F(2)=1。当 N 走到这些值的时候,函数不再往下递归,直接返回已知结果。如果没有这些终止条件,递归会一直往下调用到负数,最终栈溢出崩溃。

致命问题:指数级的时间复杂度

虽然代码简洁,但效率极低。以 fib(5) 为例,递归调用过程如下:

复制代码
                        fib(5)
                       /      \
                  fib(4)      fib(3)
                  /    \      /    \
             fib(3)  fib(2) fib(2) fib(1)
             /    \
        fib(2)  fib(1)

可以看到:

  • fib(3) 被计算了 2 次
  • fib(2) 被计算了 3 次

N 更大时,这种重复呈指数级增长。计算 fib(20) 时,fib(18) 会被重复计算成千上万次。时间复杂度约为 O(2n)O(2^n)O(2n) ,空间复杂度是递归调用栈的深度 O(n)O(n)O(n)

根本原因:存在大量重叠子问题(Overlapping Subproblems),同一个子问题被反复求解。

💡 核心心法:千万不要看不起暴力解法,动态规划问题最困难的就是写出这个暴力解法,即状态转移方程。只要写出暴力解法,优化方法无非是用"备忘录"或者 DP table,再无奥妙可言。


三、备忘录递归:自顶向下 + 记忆化

既然同一个子问题会被重复计算,那我们只要算过一次就记下来,下次直接用,不就行了?

这就是**备忘录(Memoization)**的思想:

java 复制代码
import java.util.HashMap;
import java.util.Map;

public static int fibMemo(int N) {
    Map<Integer, Integer> memo = new HashMap<>();
    return helper(N, memo);
}

private static int helper(int N, Map<Integer, Integer> memo) {
    if (N == 0) return 0;
    if (N == 1 || N == 2) return 1;
    if (memo.containsKey(N)) return memo.get(N); // 算过了,直接返回

    int res = helper(N - 1, memo) + helper(N - 2, memo); // 没算过,递归求解
    memo.put(N, res); // 算完存起来
    return res;
}

效果:

  • 每个子问题只计算一次,时间复杂度降为 O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)(备忘录哈希表 + 递归栈)

备忘录本质上还是递归,只是"带记忆的递归"。它从大问题出发,拆到小问题,碰到算过的就直接返回------这叫自顶向下


四、DP 数组:自底向上填表

如果我们把备忘录独立出来变成一张完整的表 ,然后用循环从最小的子问题开始,一步步推到最终答案,这就是动态规划(Dynamic Programming)的标准写法。

核心思想

递归(自顶向下) DP(自底向上)
从大问题出发,拆成小问题 从小问题出发,逐步合成大问题
用递归栈 用循环
按需计算(备忘录) 顺序填表

Java 实现(DP 数组版)

java 复制代码
public static int fibDP(int N) {
    if (N == 0) return 0;
    if (N == 1 || N == 2) return 1;

    // dp[i] 表示第 i 个斐波那契数 F(i)
    int[] dp = new int[N + 1]; // 长度 N+1,下标 0 ~ N,默认全 0
    // 对应 C++ 的 vector<int> dp(N + 1, 0);

    // Base cases:填好已知的"地基"
    dp[1] = 1;
    dp[2] = 1;

    // 从底向上,一步步推导
    for (int i = 3; i <= N; i++) {
        dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
    }

    return dp[N]; // 直接取答案
}

状态转移方程

书上给出的数学形式:

f(n)={1,n=1,2f(n−1)+f(n−2),n>2f(n) = \begin{cases} 1, & n=1,2 \\ f(n-1)+f(n-2), & n>2 \end{cases}f(n)={1,f(n−1)+f(n−2),n=1,2n>2

为什么叫"状态转移"?把 f(n)f(n)f(n) 想作一个状态 nnn,这个状态由状态 n−1n-1n−1 和状态 n−2n-2n−2 相加"转移"而来。状态转移方程直接代表着暴力解法,它是问题的核心。

以 N=5 走一遍

初始:dp = [0, 1, 1, 0, 0, 0]

循环轮次 i dpi = dpi-1 + dpi-2 dp 数组状态
第1轮 3 dp3 = dp2 + dp1 = 1+1 = 2 0, 1, 1, **2**, 0, 0
第2轮 4 dp4 = dp3 + dp2 = 2+1 = 3 0, 1, 1, 2, **3**, 0
第3轮 5 dp5 = dp4 + dp3 = 3+2 = 5 0, 1, 1, 2, 3, **5**

返回 dp[5] = 5

时间复杂度:O(n)O(n)O(n),空间复杂度:O(n)O(n)O(n)


五、状态压缩:空间降到 O(1)

🔑 关键洞察:根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的 DP table 来存储所有状态,只要想办法存储之前的两个状态就行了。

这就是状态压缩 :如果发现每次状态转移只需要 DP table 中的一部分,就可以缩小 DP table 的大小,只记录必要的数据。上面例子相当于把 DP table 的大小从 NNN 缩小到 2。

书上 C++ 代码

cpp 复制代码
int fib(int N) {
    if (N == 0) return 0;
    if (N == 2 || N == 1) return 1;

    int prev = 1, curr = 1;
    for (int i = 3; i <= N; i++) {
        int sum = prev + curr;
        prev = curr;
        curr = sum;
    }
    return curr;
}

Java 实现

java 复制代码
public static int fibCompressed(int N) {
    if (N == 0) return 0;
    if (N == 1 || N == 2) return 1;

    int prev = 1;  // F(n-2),初始对应 F(1)=1
    int curr = 1;  // F(n-1),初始对应 F(2)=1

    for (int i = 3; i <= N; i++) {
        int sum = prev + curr; // 当前值 = 前两项之和
        prev = curr;           // 滚动:旧的"前一项"变成"前前项"
        curr = sum;            // 滚动:当前值变成新的"前一项"
    }

    return curr;
}

以 N=5 走一遍

i sum = prev + curr prev(更新后) curr(更新后)
初始 --- 1 (F(1)) 1 (F(2))
3 1+1 = 2 1 → 1 2
4 1+2 = 3 1 → 2 3
5 2+3 = 5 2 → 3 5

返回 curr = 5

时间复杂度:O(n)O(n)O(n),空间复杂度:O(1)O(1)O(1) ------ 这是面试中最推荐的写法。

📌 延伸 :后续动态规划章节还会看到,一般来说是把一个二维的 DP table 压缩成一维,即把空间复杂度从 O(n2)O(n^2)O(n2) 压到 O(n)O(n)O(n)。状态压缩是 DP 优化中极为重要的一课。


六、四种方法完整对比

方法 核心思想 时间 空间 代码特征
暴力递归 直接翻译定义 O(2n)O(2^n)O(2n) O(n)O(n)O(n) 栈 return fib(n-1)+fib(n-2)
备忘录 自顶向下 + 记忆 O(n)O(n)O(n) O(n)O(n)O(n) Map/数组 存已算结果
DP 数组 自底向上填表 O(n)O(n)O(n) O(n)O(n)O(n) dp[i]=dp[i-1]+dp[i-2]
状态压缩 滚动变量,只留必要状态 O(n)O(n)O(n) O(1)O(1)O(1) prev, curr 两个变量

七、从斐波那契到真正的动态规划

⚠️ 重要澄清:斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值,以上旨在说明重叠子问题的消除方法,演示为得到最优解法而逐步求精的过程。

真正的动态规划问题通常包含"求最值"这一要素。比如凑零钱问题(LeetCode 322):

  • 给定 k 种硬币,面值 c1, c2, ..., ck,数量无限
  • 目标金额 amount
  • 求凑出 amount 所需的最少硬币数,凑不出返回 -1

函数签名:

java 复制代码
int coinChange(int[] coins, int amount);

示例:coins = [1, 2, 5], amount = 115 + 5 + 1 = 3

思考路径和斐波那契完全一样:

  1. 先写暴力(递归枚举所有凑法)
  2. 发现重叠子问题 → 加备忘录 / 改自底向上 DP
  3. 找状态转移dp[i] = min(dp[i-c]) + 1(对所有可行硬币 c)
  4. 看能否状态压缩(这个一维可以压缩,但保留数组语义更清晰)

八、核心心法总结

千万不要看不起暴力解法,动态规划问题最困难的就是写出这个暴力解法,即状态转移方程。只要写出暴力解法,优化方法无非是用"备忘录"或者 DP table,再无奥妙可言。
状态转移方程直接代表着暴力解法,它是问题的核心。
状态压缩:每次状态转移只需要 DP table 中的一部分,可以尝试用状态压缩来缩小 DP table 的大小,只记录必要的数据。

斐波那契数列虽然简单,但它浓缩了动态规划的全部优化思路:暴力递归 → 发现重叠子问题 → 备忘录消除重复 → DP table 系统化 → 状态压缩极致优化。当你以后面对背包、最长公共子序列、股票买卖等复杂 DP 问题时,思路都是这条路径的延伸。斐波那契就是那把打开 DP 大门的钥匙。🔑

相关推荐
wuyk55530 分钟前
9.冒泡排序:像鱼缸泡泡一样的简单排序算法
开发语言·stm32·单片机·算法·排序算法
无限码力2 小时前
8.26华为OD机试真题 新系统【字符串回文判断】
算法·华为od·华为od机考·华为od机试·华为od上机考试真题·华为od最新机试真题题解·华为od机试真题题库
玖玥拾4 小时前
LeetCode 125 验证回文串
算法·leetcode
Asize11 小时前
146. LRU 缓存
算法
Asize11 小时前
543. 二叉树的直径
算法
lemon_sjdk11 小时前
ObjectProperty
java·开发语言·算法
Zentceh12 小时前
AI-ISP在夜视机芯中的应用:从传统ISP到PixelClean全彩夜视的进化
人工智能·科技·算法·计算机视觉·车载系统·视频·智能硬件
xier_ran12 小时前
【infra之路】AWQ 详解:激活感知权重保护,让 W4A16 量化精度超越 GPTQ
线性代数·算法·机器学习·量化·infra
亦皓ai12 小时前
AI时代后端工程(二):AI把代码写得越来越快,我却越来越不敢让它直接开工了
人工智能·算法·机器学习·搜索引擎·transformer