递归与递推:从汉诺塔到斐波那契的思维训练

递归与递推的本质理解

🔄 递归与递推对比

递归 Recursion

  • ✅ 思路直观,代码简洁
  • ✅ 适合树形结构问题
  • ❌ 可能栈溢出
  • ❌ 重复计算多
  • 🎯 典型:汉诺塔、DFS

递推 Iteration

  • ✅ 效率高,无栈溢出
  • ✅ 空间优化容易
  • ❌ 思路可能不直观
  • ❌ 代码相对复杂
  • 🎯 典型:斐波那契、DP

递归和递推是信息学竞赛中最基础也是最重要的思维方式。它们看似简单,却蕴含着深刻的算法思想,是动态规划、分治算法等高级技术的基础。

递归与递推的区别与联系

特征 递归 递推
思维方式 自顶向下(Top-Down) 自底向上(Bottom-Up)
实现方式 函数调用自身 循环迭代
空间复杂度 需要调用栈O(n) 通常O(1)或O(n)
时间效率 可能有重复计算 避免重复计算
代码简洁性 通常更简洁直观 可能较复杂
适用场景 树形结构、分治 线性问题、DP

递归的三要素

1. 递归终止条件(Base Case)

没有终止条件的递归会无限循环,导致栈溢出。

2. 递归调用(Recursive Call)

问题规模必须向终止条件靠近。

3. 递归返回(Return)

处理子问题的结果,得到当前问题的解。

经典递归问题详解

问题1:阶乘计算

递归实现:


int factorial(int n) {
    // 终止条件
    if (n <= 1) return 1;
    // 递归调用
    return n * factorial(n - 1);
}

递推实现:


int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

问题2:汉诺塔问题

问题描述:有三根柱子A、B、C,A柱上有n个盘子,大盘在下小盘在上。要求将所有盘子移到C柱,每次只能移动一个盘子,且大盘不能压在小盘上。

递归思路:

  1. 将n-1个盘子从A移到B(借助C)
  2. 将最大的盘子从A移到C
  3. 将n-1个盘子从B移到C(借助A)

void hanoi(int n, char from, char to, char aux) {
    if (n == 1) {
        cout << "Move disk 1 from " << from << " to " << to << endl;
        return;
    }
    
    // 将n-1个盘子从from移到aux,借助to
    hanoi(n - 1, from, aux, to);
    
    // 移动最大的盘子
    cout << "Move disk " << n << " from " << from << " to " << to << endl;
    
    // 将n-1个盘子从aux移到to,借助from
    hanoi(n - 1, aux, to, from);
}

// 移动次数:2^n - 1

问题3:全排列生成


void permute(vector<int>& nums, int start, vector<vector<int>>& result) {
    if (start == nums.size()) {
        result.push_back(nums);
        return;
    }
    
    for (int i = start; i < nums.size(); i++) {
        swap(nums[start], nums[i]);
        permute(nums, start + 1, result);
        swap(nums[start], nums[i]);  // 回溯
    }
}

问题4:快速幂

递归实现:


long long quickPower(long long base, long long exp, long long mod) {
    if (exp == 0) return 1;
    
    long long half = quickPower(base, exp / 2, mod);
    half = (half * half) % mod;
    
    if (exp % 2 == 1) {
        half = (half * base) % mod;
    }
    
    return half;
}

递推实现:


long long quickPower(long long base, long long exp, long long mod) {
    long long result = 1;
    base %= mod;
    
    while (exp > 0) {
        if (exp & 1) {
            result = (result * base) % mod;
        }
        base = (base * base) % mod;
        exp >>= 1;
    }
    
    return result;
}

递推问题详解

斐波那契数列的多种解法

方法 时间复杂度 空间复杂度 优缺点
暴力递归 O(2^n) O(n) 简单但效率极低
记忆化递归 O(n) O(n) 避免重复计算
递推 O(n) O(n) 简单高效
空间优化递推 O(n) O(1) 最优解法
矩阵快速幂 O(logn) O(1) 适合求大数

矩阵快速幂解法:


struct Matrix {
    long long a[2][2];
    Matrix() { memset(a, 0, sizeof(a)); }
    
    Matrix operator*(const Matrix& other) const {
        Matrix result;
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) {
                for (int k = 0; k < 2; k++) {
                    result.a[i][j] += a[i][k] * other.a[k][j];
                }
            }
        }
        return result;
    }
};

long long fibonacci(int n) {
    if (n <= 1) return n;
    
    Matrix base;
    base.a[0][0] = base.a[0][1] = base.a[1][0] = 1;
    
    Matrix result;
    result.a[0][0] = result.a[1][1] = 1;  // 单位矩阵
    
    n--;
    while (n > 0) {
        if (n & 1) result = result * base;
        base = base * base;
        n >>= 1;
    }
    
    return result.a[0][0];
}

杨辉三角(组合数递推)


const int MAXN = 35;
long long C[MAXN][MAXN];

void initCombination() {
    C[0][0] = 1;
    for (int i = 1; i < MAXN; i++) {
        C[i][0] = 1;
        for (int j = 1; j <= i; j++) {
            C[i][j] = C[i-1][j-1] + C[i-1][j];
        }
    }
}

卡特兰数递推

卡特兰数在信息学竞赛中应用广泛:

  • n对括号的合法匹配数
  • n+1个节点的不同二叉搜索树数量
  • n×n格子从左下到右上不越过对角线的路径数

// 递推公式:C(n) = C(0)*C(n-1) + C(1)*C(n-2) + ... + C(n-1)*C(0)
// 或:C(n) = C(n-1) * 2*(2n-1) / (n+1)

long long catalan[MAXN];

void initCatalan() {
    catalan[0] = catalan[1] = 1;
    
    // 方法1:直接递推
    for (int i = 2; i < MAXN; i++) {
        catalan[i] = 0;
        for (int j = 0; j < i; j++) {
            catalan[i] += catalan[j] * catalan[i-1-j];
        }
    }
    
    // 方法2:公式递推
    for (int i = 2; i < MAXN; i++) {
        catalan[i] = catalan[i-1] * 2 * (2*i - 1) / (i + 1);
    }
}

递归优化技巧

1. 记忆化(Memoization)


map<pair<int, int>, int> memo;

int solve(int n, int k) {
    if (n == 0 || k == 0) return 1;
    
    auto key = make_pair(n, k);
    if (memo.count(key)) return memo[key];
    
    int result = solve(n-1, k) + solve(n, k-1);
    memo[key] = result;
    
    return result;
}

2. 尾递归优化


// 普通递归
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// 尾递归
int factorialTail(int n, int acc = 1) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, acc * n);
}

3. 递归深度控制


const int MAX_DEPTH = 10000;
int depth = 0;

bool solve(int n) {
    depth++;
    if (depth > MAX_DEPTH) {
        depth--;
        return false;  // 防止栈溢出
    }
    
    // 递归逻辑
    
    depth--;
    return true;
}

经典递归递推题目

题目1:爬楼梯问题变形

一次可以爬1级、2级或3级,但不能连续两次爬3级。


int climbStairs(int n) {
    // dp[i][0]表示爬到第i级,上一步不是3级
    // dp[i][1]表示爬到第i级,上一步是3级
    vector<vector<int>> dp(n+1, vector<int>(2, 0));
    
    dp[0][0] = 1;
    
    for (int i = 1; i <= n; i++) {
        // 从i-1爬1级,或从i-2爬2级
        if (i >= 1) dp[i][0] += dp[i-1][0] + dp[i-1][1];
        if (i >= 2) dp[i][0] += dp[i-2][0] + dp[i-2][1];
        
        // 从i-3爬3级,但上一步不能是3级
        if (i >= 3) dp[i][1] = dp[i-3][0];
    }
    
    return dp[n][0] + dp[n][1];
}

题目2:数字三角形


int maxPathSum(vector<vector<int>>& triangle) {
    int n = triangle.size();
    vector<vector<int>> dp = triangle;
    
    // 自底向上递推
    for (int i = n - 2; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            dp[i][j] += max(dp[i+1][j], dp[i+1][j+1]);
        }
    }
    
    return dp[0][0];
}

题目3:整数划分

将n划分成不超过m的正整数之和的方案数。


int partition(int n, int m) {
    if (n == 1 || m == 1) return 1;
    if (n < m) return partition(n, n);
    if (n == m) return 1 + partition(n, m-1);
    return partition(n, m-1) + partition(n-m, m);
}

// 递推版本
int partitionDP(int n, int m) {
    vector<vector<int>> dp(n+1, vector<int>(m+1, 0));
    
    for (int i = 0; i <= n; i++) dp[i][1] = 1;
    for (int j = 0; j <= m; j++) dp[1][j] = 1;
    
    for (int i = 2; i <= n; i++) {
        for (int j = 2; j <= m; j++) {
            if (i < j) {
                dp[i][j] = dp[i][i];
            } else if (i == j) {
                dp[i][j] = 1 + dp[i][j-1];
            } else {
                dp[i][j] = dp[i][j-1] + dp[i-j][j];
            }
        }
    }
    
    return dp[n][m];
}

递归递推的常见错误

错误类型及解决方案

错误类型 表现 解决方法
无终止条件 栈溢出 确保有base case
重复计算 超时 使用记忆化
边界错误 数组越界 仔细检查边界
状态定义错误 答案错误 重新分析问题
初始化错误 部分案例错误 检查初始值

练习题推荐

递归专题

难度 题目 核心考点
简单 二叉树遍历 基础递归
简单 最大公约数 欧几里得算法
中等 N皇后问题 回溯递归
中等 归并排序 分治递归
困难 正则表达式匹配 复杂递归

递推专题

难度 题目 核心考点
简单 斐波那契变形 基础递推
简单 错排问题 递推公式
中等 最长递增子序列 动态规划
中等 格子取数 二维递推
困难 状态压缩DP 高级递推

总结

递归与递推是算法思维的基础,掌握它们需要:

  1. 理解本质:递归是分解,递推是组合
  2. 掌握模式:识别问题的递归结构
  3. 优化技巧:记忆化、空间优化等
  4. 相互转换:递归与递推的灵活运用
  5. 大量练习:培养递归思维需要实践

记住,递归让复杂问题变简单,递推让计算更高效。在信息学竞赛中,灵活运用这两种思维方式,将帮助你解决更多有挑战性的问题!

Views: 0

相关文章

答复

您的邮箱地址不会被公开。 必填项已用 * 标注