【无标题】

记忆化搜索:从斐波那契数列到动态规划

一、前言:为什么要学习记忆化搜索?

在编程的世界里,我们经常会遇到需要重复计算相同问题的情况。想象一下,如果你每次做数学题时都要重新计算1+1等于多少,那会多么浪费时间啊!记忆化搜索就是一种"聪明"的编程技巧,它让计算机能够记住已经计算过的结果,避免重复劳动。

这种方法特别适合解决:

  • 需要递归求解的问题
  • 具有重叠子问题特性的问题
  • 问题可以被分解为更小的相同子问题

通过本文,即使你是编程新手,也能轻松掌握这个强大的技术!

二、斐波那契数列:从简单递归到记忆化优化

1. 什么是斐波那契数列?

斐波那契数列是一个非常有趣的数学序列,它的定义很简单:

  • 第0项是0
  • 第1项是1
  • 从第2项开始,每一项都等于前两项之和

用数学表达式表示就是:
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (当n≥2时)

这个数列看起来是这样的:0, 1, 1, 2, 3, 5, 8, 13, 21, 34…

2. 递归求解的实现(C++版)

#include <iostream>
using namespace std;

int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n-1) + fibonacci(n-2);
}

int main() {
    int n = 10;
    cout << "斐波那契数列第" << n << "项是:" << fibonacci(n) << endl;
    return 0;
}

这个实现看起来简洁明了,但它有一个严重的问题——效率极低!

3. 为什么简单递归效率低?

让我们画一个调用图来看看计算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的增大,重复计算的次数会呈指数级增长!

时间复杂度分析:

  • 每次调用会产生2个新的调用
  • 对于n,调用次数大约是2^n
  • 所以时间复杂度是O(2^n) —— 这太可怕了!

4. 优化思路:记住已经计算过的结果

既然问题在于重复计算,那么很自然的想法就是:如果我们能把计算过的结果存起来,下次需要时直接使用,不就能大大提高效率了吗?

这就是记忆化搜索的核心思想!

三、记忆化搜索详解

1. 记忆化斐波那契数列实现(C++版)

#include <iostream>
#include <unordered_map>
using namespace std;

unordered_map<int, int> memo;

int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    // 如果已经计算过,直接返回存储的结果
    if (memo.find(n) != memo.end()) {
        return memo[n];
    }
    // 否则计算并存储结果
    memo[n] = fibonacci(n-1) + fibonacci(n-2);
    return memo[n];
}

int main() {
    int n = 50;  // 试试更大的数字!
    cout << "斐波那契数列第" << n << "项是:" << fibonacci(n) << endl;
    return 0;
}

这个版本有什么改进?

  1. 我们使用了一个哈希表memo来存储已经计算过的结果
  2. 每次计算前先检查是否已经计算过
  3. 如果没有计算过才进行递归计算,并把结果存储起来

时间复杂度分析:

  • 现在每个fib(i)只会被计算一次
  • 共有n个不同的值需要计算
  • 所以时间复杂度降为O(n) —— 巨大的进步!

2. 记忆化搜索的工作原理图解

让我们看看计算fib(5)时记忆化搜索是如何工作的:

  1. 计算fib(5),发现没存储过
  2. 需要计算fib(4)和fib(3)
  3. 计算fib(4),发现没存储过
    • 需要计算fib(3)和fib(2)
  4. 计算fib(3),发现没存储过
    • 需要计算fib(2)和fib(1)
  5. 计算fib(2),发现没存储过
    • 需要计算fib(1)和fib(0)
  6. fib(1)和fib(0)是基本情况,直接返回1和0
  7. 存储fib(2)=1
  8. 返回fib(3)=fib(2)+fib(1)=1+1=2,存储fib(3)=2
  9. 计算fib(4)=fib(3)+fib(2)=2+1=3,存储fib(4)=3
  10. 计算fib(5)=fib(4)+fib(3)=3+2=5,存储fib(5)=5

可以看到,每个子问题只计算了一次,之后都是直接从memo中获取结果。

3. 记忆化搜索的通用框架

记忆化搜索可以应用于很多递归问题,它的通用框架如下:

// 1. 定义记忆化数据结构(通常是哈希表或数组)
unordered_map<参数类型, 结果类型> memo;

返回值类型 函数名(参数) {
    // 2. 基本情况处理
    if (是基本情况) {
        return 基本情况结果;
    }
    
    // 3. 检查是否已经计算过
    if (memo.find(参数) != memo.end()) {
        return memo[参数];
    }
    
    // 4. 递归计算并存储结果
    返回值类型 结果 = 根据子问题组合得到的结果;
    memo[参数] = 结果;
    
    return 结果;
}

4. 记忆化搜索的三大优势

  1. 时间复杂度大幅降低:从指数级降到多项式级
  2. 代码改动小:只需添加记忆化逻辑,不改变原有递归结构
  3. 更符合人类思维:自上而下的思考方式,比自下而上的动态规划更直观

四、记忆化搜索的实际应用

1. 爬楼梯问题

问题描述:假设你正在爬楼梯,每次你可以爬1阶或2阶。问到达第n阶有多少种不同的方法?

分析:这其实就是斐波那契数列的变种!到达第n阶的方法数等于:

  • 从n-1阶爬1阶上来
  • 从n-2阶爬2阶上来
    所以f(n) = f(n-1) + f(n-2)

C++实现

#include <iostream>
#include <unordered_map>
using namespace std;

unordered_map<int, int> memo;

int climbStairs(int n) {
    if (n == 1) return 1;
    if (n == 2) return 2;
    if (memo.find(n) != memo.end()) {
        return memo[n];
    }
    memo[n] = climbStairs(n-1) + climbStairs(n-2);
    return memo[n];
}

int main() {
    int n = 10;
    cout << "爬" << n << "阶楼梯有" << climbStairs(n) << "种方法" << endl;
    return 0;
}

2. 网格路径问题

问题描述:在m×n的网格中,从左上角走到右下角,每次只能向右或向下移动,问有多少种不同的路径?

分析:到达(i,j)的路径数等于:

  • 从上方(i-1,j)下来的路径数
  • 从左方(i,j-1)过来的路径数
    所以f(i,j) = f(i-1,j) + f(i,j-1)

C++实现

#include <iostream>
#include <unordered_map>
#include <utility>  // for pair
using namespace std;

struct pair_hash {
    template <class T1, class T2>
    size_t operator() (const pair<T1, T2>& p) const {
        auto h1 = hash<T1>{}(p.first);
        auto h2 = hash<T2>{}(p.second);
        return h1 ^ h2;
    }
};

unordered_map<pair<int, int>, int, pair_hash> memo;

int uniquePaths(int m, int n) {
    if (m == 1 || n == 1) return 1;
    pair<int, int> key = {m, n};
    if (memo.find(key) != memo.end()) {
        return memo[key];
    }
    memo[key] = uniquePaths(m-1, n) + uniquePaths(m, n-1);
    return memo[key];
}

int main() {
    int m = 3, n = 7;
    cout << m << "x" << n << "网格的独特路径数:" << uniquePaths(m, n) << endl;
    return 0;
}

3. 0-1背包问题

问题描述:给定一组物品,每种物品都有自己的重量和价值。在限定的总重量内,如何选择物品使得总价值最大?

分析:对于每个物品,我们有两种选择:拿或者不拿。我们可以用记忆化搜索来记录已经计算过的子问题。

C++实现

#include <iostream>
#include <vector>
#include <unordered_map>
#include <utility>
using namespace std;

struct pair_hash {
    template <class T1, class T2>
    size_t operator() (const pair<T1, T2>& p) const {
        auto h1 = hash<T1>{}(p.first);
        auto h2 = hash<T2>{}(p.second);
        return h1 ^ h2;
    }
};

unordered_map<pair<int, int>, int, pair_hash> memo;

int knapsack(const vector<int>& weights, const vector<int>& values, int W, int i) {
    if (i == 0 || W == 0) return 0;
    pair<int, int> key = {W, i};
    if (memo.find(key) != memo.end()) {
        return memo[key];
    }
    
    if (weights[i-1] > W) {
        memo[key] = knapsack(weights, values, W, i-1);
    } else {
        int notTake = knapsack(weights, values, W, i-1);
        int take = values[i-1] + knapsack(weights, values, W - weights[i-1], i-1);
        memo[key] = max(notTake, take);
    }
    
    return memo[key];
}

int main() {
    vector<int> values = {60, 100, 120};
    vector<int> weights = {10, 20, 30};
    int W = 50;
    cout << "背包最大价值:" << knapsack(weights, values, W, values.size()) << endl;
    return 0;
}

五、记忆化搜索的局限性及优化

虽然记忆化搜索非常强大,但它也有一些局限性:

  1. 递归深度限制:递归太深可能导致栈溢出
  2. 空间开销:需要存储所有子问题的结果
  3. 函数调用开销:递归调用比迭代开销大

对于这些问题,我们可以考虑:

  1. 改为迭代实现(动态规划):
int fibonacci(int n) {
    if (n <= 1) return n;
    vector<int> dp(n+1);
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}
  1. 空间优化:有时候我们不需要存储所有结果
int fibonacci(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        int c = a + b;
        a = b;
        b = c;
    }
    return b;
}

六、总结

记忆化搜索是一种极其强大的算法优化技术,它的核心思想是 “以空间换时间”。通过存储已经计算过的子问题结果,可以避免重复计算,将许多指数级复杂度的问题降为多项式级。

关键要点:

  1. 记忆化搜索 = 递归 + 记忆存储
  2. 适用于有重叠子问题的情况
  3. 实现简单,通常只需添加几行代码
  4. 可以很容易地转化为动态规划解法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值