# 递推和递归:一个自下而上,一个自上而下

请添加图片描述

# 解决问题的思路不同

递推和递归是常见的解决问题的思路,在很多算法中都会用到,例如DFS算法基于递归实现,动态规划算法基于递推实现

递推是从上到下解决问题,如想知道一个国家有多少人,我们从村这个一个行政级别开始,依次向上汇报,最后到国这个级别就能知道一个国家有多少人

递归是从上到下解决问题,如想知道一个国家有多少人。国家发布命令让各个省上报各自的人数,各个省又发布命令让各自市上报各自的人数,依次类推,最后到达村这个级别,开始统计自己的人数(递推出口),统计完后逐级上报,最后到国这个级别就能知道一个国家有多少人

请添加图片描述

# 爬楼梯

题目来源:Leetcode 70 题目描述: 在这里插入图片描述

我们依次从第1阶开始依次计算方法,第1阶有1种方法,第2阶有2种方法,那么第3阶呢?

第3阶只能从第1阶或者第2阶踏过来,所以到第3阶的方法数为到第1阶或者到第2阶的方法数之和。 第4阶只能从第2阶或者第3阶踏过来,所以到第4阶的方法数为到第2阶或者到第3阶的方法数之和。

依次类推dp[i] = d[i - 1] + dp[i - 2](dp[i]为到第i阶的方法数)

学了动态规划后,你就能会发现递推就是简单版的动态规划

public int climbStairs(int n) {
    if (n == 1 || 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阶的方法数为到n-1阶和到n-2阶的和,递归的出口是第1阶有1种方法,第2阶有2种方法。

public int climbStairs(int n) {
    if (n == 1 || n == 2) {
        return n;
    }
    return climbStairs(n - 1) + climbStairs(n - 2);
}

在大多数情况下,递推解决问题的思路比较简单,代码也比较简洁,但是效率比较低(函数调用),同时也会有很多重叠子问题。递推的实现思路比较复杂,但是效率比较高。 参见二叉树的前中后序的递归调用和非递归调用

请添加图片描述 如图,当通过递归的方式求到第5级台阶的方法数时,到第3级台阶的方法数被算了2次,所以我们可以使用额外空间将结算得到的结果缓存下来,避免重复计算(即备忘录算法)

class Solution {

    // 数组大小按照题目的n确定
    int[] visit = new int[100];

    public int climbStairs(int n) {
        if (visit[n] != 0) {
            return visit[n];
        }
        if (n == 1 || n == 2) {
            return n;
        }
        visit[n] = climbStairs(n - 1) + climbStairs(n - 2);
        return visit[n];
    }

}

# 不同路径

来源:LeetCode 62 描述: 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径? 在这里插入图片描述

思路:这个大家一下就会想到用递归解决,假设f(m,n)表示移动到点(m,n)的路径数,因为机器人智能向下或者向右移动,所以点(m,n)只能从点(m-1,n)和(m,n-1)移动而来,递归公式就是f(m,n)=f(m-1,n)+f(m,n-1),递归的出口呢?当然就是网格的边界了,网格边界上的点都只有一种方法,按照这种思路写出来如下代码

class Solution {
   public int uniquePaths(int m, int n) {
       // 在网格边界的格子只能有一种走法
       if (m == 1 || n == 1) {
           return 1;
       }
       // m,n这个位置只能从(m - 1 , n)和(m, n - 1)移动而来
       return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
   }
}

其实这个代码效率还是很低的,因为有很多重复的计算,如下图 在这里插入图片描述 当m和n为(3,3)时,(2,2)被计算了2次,而且m和n越大,重复计算的次数最多,我们可以把已经算出来的值保存一下,这样下次再用的时候就不用算了,直接取就行,叫做备忘录算法,grid[m][n]表示走到(m,n)这个点时的路径数。

class Solution {
   
   public static int[][] grid = new int[110][110];

   public int uniquePaths(int m, int n) {
       if (grid[m][n] != 0)
           return grid[m][n];
       if (m == 1 || n == 1) {
           return 1;
       }
       return grid[m][n] = uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
   }
}

当值不为0的时候说明已经被算过了,直接取就行了,否则就得计算并保存结果,这样效率提高了不少,但是如果m和n特别大,递归层数过多时会造成堆栈溢出的,该怎么办?这个时候就得用到动态规划了

递归是从上至下开始计算的,有没有可能从下而上的计算呢?,如先算出(1,2)和(2,1),然后就能算出(2,2)了,我们得按照一定的规律计算,保证在算(2,2)之前,(1,2)和(2,1)已经算完了,我们只要按行从左到右计算,或者按列从上到下即可

dp[i][j]表示到达第i行第j列的路径数,所以状态转移方程为

dp[i][j] = dp[i][j-1] + dp[i-1][j]
class Solution {

   public static int[][] grid = new int[110][110];

   public int uniquePaths(int m, int n) {

       for (int i = 1; i <= n ; i++) {
           for (int j = 1; j <= m ; j++) {
               if (i == 1 || j == 1)
                   grid[i][j] = 1;
               else
                   grid[i][j] = grid[i][j-1] + grid[i-1][j];
           }
       }
       return grid[n][m];
   }
}

# 数的计算

题目地址:洛谷 p1028 题目描述: 在这里插入图片描述 在这里插入图片描述

int main() {
    int n;
    cin >> n;
    int dp[n + 1];
    for (int i = 1; i <= n; ++i) {
        dp[i] = 1;
        for (int j = 1; j <= i / 2; ++j) {
            dp[i] += dp[j];
        }
    }
    cout << dp[n] << endl;
    return 0;
}

# 传球游戏

题目地址:洛谷 p1057 题目描述: 在这里插入图片描述

#include <iostream>
using namespace std;

const int MAX_N = 30;
int dp[MAX_N + 5][MAX_N + 5] = {0};

int main() {
    int n, m;
    cin >> n >> m;
    dp[0][1] = 1;
    for (int i = 1; i <= m; ++i) {
        for (int j = 2; j <= n - 1; ++j) {
            dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
        }
        dp[i][1] = dp[i - 1][2] + dp[i - 1][n];
        dp[i][n] = dp[i - 1][1] + dp[i - 1][n - 1];
    }
    cout << dp[m][1] << endl;
    return 0;
}