递推详解和经典例题

82 阅读5分钟

1、递推是什么

递推就是“用前面的结果,推算后面的结果”,一步一步往前推。

从下到上 从已知到未知解决问题

数学中的递推例子

  • 等差数列:aₙ = aₙ₋₁ + d,比如a₁=2,d=3,a₂=5,a₃=8……
  • 斐波那契数列:F(n) = F(n-1) + F(n-2),先算F(1)、F(2),再一步步推到F(3)、F(4)……

2、递推的关键

  • 有起点(初始值):要知道最开始的一个或几个数。
  • 一步步往后推:每一步都用前面的结果。

3、经典例题

斐波那契数列:有一个数列:0、1、1、2、3、5、8、13、21、34...

public static int fibonacciDP(int n) {
        if (n <= 1) return n;        
        int[] dp = new int[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];
    }

变式

已知 f(n)f(1)=1;     

n=1f(2)=1;

n=2f(3)=1;

n=3f(n)=2f(n-1)+3f(n-2) +f(n-3)   n>=4

    public static int fWithArray(int n) {
        if (n <= 0) {
            throw new IllegalArgumentException("n必须大于0");
        }     
        if (n == 1 || n == 2 || n == 3) {
            return 1;
        }       
        // 使用数组存储所有计算结果
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 1;
        dp[3] = 1;        
        for (int i = 4; i <= n; i++) {
            dp[i] = 2 * dp[i - 1] + 3 * dp[i - 2] + dp[i - 3];
        }        
        return dp[n];
    }

爬楼梯问题:有 n 阶楼梯,每次可以爬 1 或 2 个台阶,有多少种不同的方法爬到楼顶?

public static int climbStairs(int n) {
        if (n <= 2) return n;        
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }        
        return dp[n];
    }

母牛的故事

有一头母牛,从第二年起,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。请编程实现在第n年的时候,共有多少头母牛?

先找规律

image.png

重点是找到关系式:f[n]=f[n-1]+f[n-3]

public static int calculateSequence(int n) {
        if (n <= 0) {
            return 0;
        }       
        int[] f = new int[n];        
        if (n >= 1) f[0] = 1;
        if (n >= 2) f[1] = 2;
        if (n >= 3) f[2] = 3;        
        for (int i = 3; i < n; i++) {
            f[i] = f[i-1] + f[i-3];
        }        
        return f[n-1];
    }

杨辉三角

image.png

public static void printTriangle(int n) {
        for (int i = 0; i < n; i++) {
            int number = 1;
            
            for (int j = 0; j < n - i - 1; j++) {
                System.out.print("  ");
            }
            
            for (int j = 0; j <= i; j++) {
                System.out.printf("%4d", number);
                number = number * (i - j) / (j + 1);
            }
            System.out.println();
        }
    }

不同路径问题

机器人从 m×n 网格的左上角到右下角,只能向右或向下移动,有多少条不同路径?

机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。

确定dp数组(dp table)以及下标的含义:dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。

确定递推公式:想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。

dp数组的初始化:如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。

确定遍历顺序:这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。

 public static int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        
        // 初始化第一行和第一列
        for (int i = 0; i < m; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j < n; j++) {
            dp[0][j] = 1;
        }
        
        // 递推计算
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        
        return dp[m - 1][n - 1];
    }

硬币找零问题

给定不同面额的硬币和总金额,计算可以凑成总金额的最少硬币数

public static int coinChange(int[] coins, int amount) {
        // dp[i] 表示凑成金额 i 所需的最少硬币数
        int[] dp = new int[amount + 1];
        
        // 初始化,将 dp 数组填充为最大值
        Arrays.fill(dp, amount + 1);
        dp[0] = 0;  // 金额为 0 时不需要任何硬币
        
        // 遍历所有金额
        for (int i = 1; i <= amount; i++) {
            // 遍历所有硬币
            for (int coin : coins) {
                if (coin <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }
        
        return dp[amount] > amount ? -1 : dp[amount];
    }

4、递推和递归的区别

特性递推 (Iteration)递归 (Recursion)
基本思想从已知条件出发,逐步推导未知结果将问题分解为更小的同类子问题
执行方式循环结构(for/while)函数自我调用
方向自底向上(Bottom-up)自顶向下(Top-down)
内存使用通常较低,只存储必要状态较高,需要维护调用栈
性能通常更高效,无函数调用开销可能有重复计算,函数调用有开销
代码可读性相对直观,但可能冗长更符合数学定义,简洁优雅
适用场景线性问题、状态转移明确的问题树形结构、分治问题、回溯问题
栈溢出风险高(深度过大时)
实现难度容易理解和调试需要理解递归树和终止条件