算法学习之动态规划,从 Fibonacci 到 01 背包与 LeetCode 实战

今天开始我将开始学习一些系统性算法,开发当然很重要,但是,算法也是很重要,今后,我也会将算法学习纳入我心中比较重要的地位。我虽然之前也得过一些算法方面的奖项,但是我都是感觉凭借数感和一些小聪明去解决的,一遇到正规军题目,就无解了。从今天开始,我将重视算法方面的研究,每天至少花上几个小时吧,系统性学习一些算法。

今天我学习的算法是动态规划。说来也好笑,其实这些内容在研一上算法课时,都是讲过的,当时学习的时候也没怎么放在心上,也觉得算法不是那么重要,一直学得囫囵吞枣的,只是为了考试而去学习算法,只看到它的表层,根本不会深究它的原理。

动态规划在我的理解里,当问题满足最优子结构且存在大量重叠子问题时,如果使用暴力计算的方式解决问题,极易导致重复计算,导致高复杂度,这时候,使用动态规划就是一个好选择。那么,什么是最优子结构重叠子问题?说人话就是一个问题的最优解,可以从它的子问题的最优解中得出,且子问题被重复计算。

大致上,动态规划有两种:

  1. 自顶向下的

  2. 自底向上的

那么,如何判断需要使用动态规划?主要有下面几点:

  1. 可以把问题拆分成若干子问题

  2. 能否写出状态和状态转移(就是通用的表达式,后面会提)

  3. 能否定义边界并确定计算顺序

以下是我在完成今天的学习之后,总结的动态规划解题的步骤:

  1. 确定要优化的量(求最大值或者且最小值,就是要优化的量)

  2. 定义状态(这个状态一般是个数组,dp[],可能一维,也可能二维等等)

  3. 写状态转移方程(就是通用的表达式,即如何从一个小问题中,得到大的问题)

  4. 确定边界值(如 dp[0] 的值等等,这个边界代表的是什么时候结束或开始)

  5. 选择计算顺序(如自顶向下的,或自底向上的)

我们先从常见的基础入门。

1 斐波那契

在数学上,有一个斐波那契数列,他是长这样的:

0,1,1,2,3,5,8,13,21,34,55......

斐波那契数列,就是从第三项开始,每一项都是前面两项的和。

而我们的题目,也随之而来,即求第 n 项斐波那契数列的值。

我们按照我的解题步骤来:

  1. 确定要优化的量

    第 n 项就是我们要优化,即计算的量。

  2. 定义状态

    我们创建一个静态数组,来保存状态,其实,这个状态数组就是我们的斐波那契数列:

    private static long[] dp = new long[100];
  3. 写状态转移方程

    状态转移方程就是通用的表达式,其实很简单,就是:

    f(n) = f(n-1) + f(n-2)   (n>=3时)
  4. 确定边界值

    在本题中,边界值也很好确认,即第 1 项是 0,第 2 项是 1。也就是 f(1)=0,f(2)=1。

  5. 选择计算顺序

    这个计算顺序我们在写完程序之后再说,看看我们的计算顺序是什么,同时,另一种计算顺序又该怎么做?

来看我们的程序,程序非常简单:

package dp;


import java.util.Arrays;

/**
* @Author: sangui
* @CreateTime: 2025-11-11
* @Description: 斐波那契 0、1、1、2、3、5、8、13、21、34
* @Version: 1.0
*/
public class Fibonacci {
   private static long[] dp = new long[100];

   static void main() {
       // 默认全是 -1
       Arrays.fill(dp, -1);
       // 第一项是 0,第二项是 1
       dp[0] = 0;
       dp[1] = 1;

       System.out.println(getFibonacci(10));
  }

   /**
    * 获取某一项斐波那契数组的值
    *
    * @param n 第 n 项
    * @return 第 n 项斐波那契数组的值
    */
   public static long getFibonacci(int n) {
       // 初始数组值都是 -1,若不是 -1,代表已经该项数已经计算并记录在对应数组了
       if (dp[n] != -1) {
           return dp[n];
      }
       // 运行到这里意味着还没用算过第 n 项,开始计算第 n 项
       dp[n] = getFibonacci(n - 1) + getFibonacci(n - 2);
       return dp[n];
  }
}

如果我们的 dp 数组没有赋值过,那么我们数组中元素就都是默认值 0 。为了方便,也同时是因为我们的斐波那契数列中的数都是非负的,所以我们将数组的所有值赋值为 -1,代表这一项没有被赋值计算过。根据我们的边界值的确定,我们直接写:dp[0] = 0;dp[1] = 1;

接下来看具体的方法。我们的想法是,一项一项计算数列的每一项,把所有已经计算过的值,存入 dp 数组里,若某一项被赋值计算过,则直接返回这个值。至于未计算的值,则使用我们想好的 状态转移方程来计算,并同时存储他的值。

程序就是这样,很简单。我们来分析一下,他是自顶向下的,还是自底向上的。明显这时自顶向下的,因为我们是最先计算第 n 项,再一步一步按照转移方程来计算前两项,直至到我们的边界值。

那要是自底向上的,程序应该是怎么样的?其实也差不多,自底向下就是它是最先计算下面的值,再依次往上计算,请看我新写的程序:

/**
* 获取某一项斐波那契数组的值(自底向上)
*
* @param n 第 n 项
* @return 第 n 项斐波那契数组的值
*/
public static long getFibonacci2(int n) {
   // 初始数组值都是 -1,若不是 -1,代表已经该项数已经计算并记录在对应数组了
   if (dp[n] != -1) {
       return dp[n];
  }
   // 对第 2 项到第 n 项的值进行计算
   for (int i = 2; i <= n; i++) {
       // 若该值是 -1 ,则代表该值需要计算,反之则不用
       if (dp[i] == -1) {
           // 计算该值
           dp[i] = dp[i - 1] + dp[i - 2];
      }
  }
   return dp[n];
}

我们看到,他里面用到了循环,而不是递归!这是一个关键点,其次,它在循环里,从小到大,计算斐波那契数列中的值,并同时记录下 dp 数组 具体的值。

可以得出结论:

自顶向下用了递归,逻辑简洁,但可能会方法栈溢出,自底向上稍复杂,但无溢出风险,因为用的是循环,次数可控。

2 爬楼梯

题目: 一共有 n 阶台阶,你每次可以爬 1 阶或 2 阶。请问有多少种不同的方式爬到顶?

爬楼梯这一题,这个题我之前遇到过,我知道解法和斐波那契的算法一模一样,一开始我还绕不过弯来,为什么他和斐波那契数列一样。

  1. 确定要优化的量

    和上一题一样,第 n 项就是我们要优化,即计算的量。

  2. 定义状态

    我们创建一个静态数组,来保存状态,其实,这个状态数组就是我们走到某一阶台阶时,存储的有多少种不同的方式:

    static int[] arr = new int[46];
  3. 写状态转移方程

    这一步是解决题目的关键,就是要想清楚一件事,就是,假设有 n = 5 级台阶,你要到达第 5 级台阶的最后一步,只有两种可能:

    1. 从第 4 级台阶走 1 级上来

    2. 从第 3 级台阶走 2 级上来

    不可能有第三种情况!也就是说,当我们走第 n 阶台阶,一定是从 n-1 阶台阶和 n-2 阶台阶走来的,那么走第 n 阶台阶的走法,就是 n-1 阶台阶的走法加上 n-2 阶台阶的走法!当然 n是要大于 2 的。所以我们归纳状态转移方程:

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

    可以看到,又是这个表达式!

  4. 确定边界值

    我们确定了 n>=3时的状态转移方程,那么,很容易就想到,n=1 和 n=2 就是它的边界值,即第 1 项是 1,第 2 项是 2。也就是 f(1)=1,f(2)=2。代表的是1 阶台阶有一种走法,2 阶台阶有两种走法

  5. 选择计算顺序

    计算顺序还是有两种。我们仔细看,究竟是自顶向下的好,还是自底向上的好。

来看我写的程序:

package dp;


import java.util.Arrays;

/**
* @Author: sangui
* @CreateTime: 2025-11-11
* @Description: 爬楼梯
* @Version: 1.0
*/
public class Q_70 {
   // 状态数组,我自己写的,最高 45 阶台阶
   static int[] arr = new int[46];

   static void main() {
       // 题目:一共有 n 阶台阶,你每次可以爬 1 阶或 2 阶。请问有多少种不同的方式爬到顶?

       // n 是题目输入,代表有 n 阶台阶
       int n = 10;

       // 计算结果
       int res = climbStairs(n);
       // 输出结果
       System.out.println(res);
  }

   /**
    * 爬楼梯的方法
    *
    * @param n 代表 n 阶台阶
    * @return 爬 n 阶台阶的有多少种不同的方式
    */
   public static int climbStairs(int n) {
       if (n == 1) {
           // 1 阶台阶有一种走法
           arr[n] = 1;
           return arr[n];
      }
       if (n == 2) {
           // 2 阶台阶有两种走法
           arr[n] = 2;
           return arr[n];
      }

       if (arr[n] != 0) {
           return arr[n];
      }

       // 第 n 阶台阶(n>2),一定是从 n-1 阶台阶和 n-2 阶台阶走来的
       // 那么走第 n 阶台阶的走法,就是 n-1 阶台阶的走法加上 n-2 阶台阶的走法
       arr[n] = climbStairs(n - 1) + climbStairs(n - 2);

       return arr[n];
  }
}

程序就不多说了,和斐波那契数列几乎一模一样,且这个是自顶向下的解法。

同时,我看到了一个天才的程序,我们目前的程序的空间复杂度是 O(n),也就是说随着我们楼梯的数量而增加,我看到了一个空间复杂度是 O(1)的程序,供大家参考:

 /**
* 爬楼梯的方法(邪修,无需数组存储)
*
* @param n 代表 n 阶台阶
* @return 爬 n 阶台阶的有多少种不同的方式
*/
public static int climbStairs3(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int a = 1;
int b = 2;
int c = 0;
for (int i = 3; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return c;
}

最后,我提出一个进阶版爬楼梯题:

每次可以爬 1、2 或 3 阶,问有多少种方法?

这个题就不再是单纯斐波那契,而是根据自己推状态方程的训练,我会在文末贴上我的解题方案(分自顶向下、自底向上、还有空间复杂度为 O(1) 的邪修)、

3 0/1 背包

0/1 背包是经典中的经典,理解它能把很多变形题都拆开来做。

n 件物品,第 i 件重量 wt[i],价值 val[i],背包容量 W。每件物品最多取 1 次(要么取,要么不取)。求在不超过容量 W 的前提下,能获得的最大总价值是多少?并演示如何恢复出被选择的物品集合。

下面直接给出我的题目数据,即总共有 4 件物品,背包的容量是 10 :

// 重量
int[] wt = {3, 4, 6, 5};
// 价值
int[] val = {2, 3, 1, 4};
// 容量
int W = 10;

这题目看起来很难,但不要被题目吓到了。提示一点,要把问题拆分成不选的两种选择,我们还是根据之前的五个步骤来分析题目

  1. 确定要优化的量

    要优化的量其实就是题目中的要求的值。

  2. 定义状态

    要用一个数组来记录中间结果。常见定义是:

    static int[][] dp = new int[4 + 1][10 + 1]

    为方便和展示友好起见,题目中给的 4 和 10 我直接硬编码写入了,后面的我也会这么做。

    注意这里的 dp 是个 二维数组,比如 dp[i][w] 的含义是,只考虑前 i 件物品、背包容量为 w 时能取得的最大价值。而我们要求的,即是 dp[4][10] ,即代表,前 4 件物品,背包容量为 10 时的最大价值。

  3. 写状态转移方程

    很关键的一步!要把问题拆分成不选的两种选择。

    • 不选

      如果选择不选的话,表达式很好写:

      dp[i][w] = dp[i-1][w]

      什么意思,我们依次循环 i ,i 是我们的物品,有 4 件,从 1-4 循环遍历 ,若不选,那么他的价值就和上一个存储的值一样。从这也可以看出,我们的计算顺序,是从小到大,就是自底向上的。

    • 如果选择选的话,那么对应的 dp[i][w] 又是什么呢?我们的想法是,若我们选了,那么该值应该是 没加这件物品之前的价值 + 这件物品的价值

      其实就是:

      dp[i][w] = dp[i-1][w - wt[i-1]] + val[i-1]

      注意 dp[i-1][w - wt[i-1]] 就是 没加这件物品之前的价值

      val[i-1] 就是 这件物品的价值,注意坐标偏移, i - 1 其实代表的就是我的第 i 件商品。

    在做了选与不选的选择之后,我们就要比较,取两者之中最大者(不要忽视了这一步,以为选了一定比不选大,他们不能这么比)

  4. 确定边界值

    想想 i = 0w = 0 时应该是什么值? 其实,没物品或者容量为 0,都没价值可取,即都是 0.

  5. 选择计算顺序

    是自底向上的,在之前上面的步骤中说过。根据状态定义,dp[i][w] 依赖 dp[i-1][...],所以应该 外层遍历 i 从 1 到 n,内层遍历 w 从 0 到 W

最后,我给出我的程序:

package dp;


/**
* @Author: sangui
* @CreateTime: 2025-11-11
* @Description: 0-1 背包
* @Version: 1.0
*/
public class Knapsack01 {
static int[][] dp = new int[4 + 1][10 + 1];

static void main() {
// 有 n 件物品,第 i 件重量 wt[i],价值 val[i],背包容量 W。
// 每件物品最多取 1 次(要么取,要么不取)。
// 求在不超过容量 W 的前提下,能获得的最大总价值是多少?
// 并演示如何恢复出被选择的物品集合。

// 重量
int[] wt = {3, 4, 6, 5};
// 价值
int[] val = {2, 3, 1, 4};
// 容量
int W = 10;
// 计算最大价值
int res = knapsackWithChoice(wt, val, W);
// 输出
System.out.println("计算得出最大的背包价值:" + res);
}

public static int knapsackWithChoice(int[] wt, int[] val, int W) {
// i 代表第 i 件物品,i 从 1 开始。
for (int i = 1; i <= wt.length; i++) {
// 第 i 件物品的重量
int wt_i = wt[i - 1];
// 第 i 件物品的价值
int val_i = val[i - 1];
// w 代表背包容量,从 0 开始计算,一直到我们的真正的背包容量:W
for (int w = 0; w <= W; w++) {
// 若 背包容量 w 小于 第 i 件物品的重量,代表不能放
if (w < wt_i) {
// 不能放,该价值等于上一件物品这时的价值
dp[i][w] = dp[i - 1][w];
} else {
// 走到这里,说明背包有空闲重量,可放,可不放,需要比较这两个值
// 该价值等于 没加这件物品的价值(dp[i - 1][w - wt_i]) + 这件物品的价值(val_i)
dp[i][w] = Math.max(dp[i - 1][w - wt_i] + val_i, dp[i - 1][w]);
}
}
}
System.out.println("具体 dp 矩阵:------------------");
for (int i = 0; i <= 4; i++) {
for (int j = 0; j <= 10; j++) {
System.out.print(dp[i][j] + " ");
}
System.out.println();
}
System.out.println("------------------------------");
return dp[4][10];
}
}

在程序中,我们依照自底向上循环,依次进行遍历,需要注意的是,影响我们选与不选的,还有一种情况,就是,若背包容量超了,我们必须不选。若没抄,我们再把之前分析的两个值取最大值分析。最后使出我们 dp 数组最最边界的值,就是我们所求。

至此,程序就解决了,但同时,0/1 背包问题往往还伴随着另外一个问题,就是,我们需要回溯出,到底选了什么物品。恢复被选择的物品的思路就是:

  1. 从最后一件物品往前走:i = n,容量 w = W

  2. 如果 dp[i][w] != dp[i-1][w],说明第 i 件物品被选了

    • 然后把 w -= wt[i-1]

  3. 否则没选,直接 i--

我给出我的程序:

for (int i = 4; i >= 1; i--) {
if (dp[i][W] != dp[i - 1][W]) {
System.out.println("第 " + i + " 件物品被选!");
W -= wt[i - 1];
}
}

这端代码写在生成矩阵之后,返回最优质之间即可。

0/1背包的变式:一维滚动数组版本

0/1背包问题,除了二维 DP 的解决方法,其实还有一维滚动数组版本,就是我们的 dp 数组,改为一维的。

我们看之前二维 DP 时的状态:

dp[i][w] = 前 i 件物品、背包容量为 w 的最大价值

观察递推格式:

dp[i][w] = max(dp[i-1][w], dp[i-1][w - wt[i-1]] + val[i-1])

dp[i][w] 只依赖 上一行 dp[i-1][...],所以完全可以用一维数组 dp[w] 来替代。

所以二维的 i 这一维度,其实可以去掉。我们可以建立一个一维 dp 数组来解决,以减少内存复杂度。

而一维数组的递推方向则是关键,之前我们是自底向上的,一维数组中,则是要大到小。因为如果从小到大遍历,dp[w - wt[i-1]] 会被覆盖,导致把同一件物品选了多次(变成完全背包)。从大到小就保证了每件物品只被用一次。

来看我写的代码:

static int[] dp = new int[10 + 1];

public static int knapsackWithChoice(int[] wt, int[] val, int W) {
// i 代表第 i 件物品,i 从 1 开始。
for (int i = 1; i <= 4; i++) {
// 第 i 件物品的重量
int wt_i = wt[i - 1];
// 第 i 件物品的价值
int val_i = val[i - 1];
for (int w = W; w >= wt[i-1]; w--) {
dp[w] = Math.max(dp[w], dp[w - wt_i] + val_i); // 最关键一行
}
}
System.out.println("具体 dp 矩阵:------------------");
for (int i = 0; i <= 10; i++) {
System.out.print(dp[i] + " ");
}
System.out.println();
System.out.println("------------------------------");

return dp[10];
}

其中,最关键的一行就是:

dp[w] = Math.max(dp[w], dp[w - wt_i] + val_i); // 最关键一行

其实,想通了就很简单,这和之前的二维 dp 大差不差,就是把 i 给去掉了,但实质还是一样的。

最后是回溯选中物品,在二维 dp 中,我们是要根据二维 dp 中的 i 行来判断是否选中的。一维 DP 也可以回溯,但回溯稍微麻烦一些,必须在更新前保存“上一步状态”。一般不会要求一维数组的追溯,因为一维数组主要用于“求最大价值”,空间优化”。

4 一和零

这是今天力扣的每日一题,题目是这样的:

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

这题就是典型的 0/1 背包问题的变种,但他有两个背包,分别是存 0 和 存 1的,容量由 m 和 n 来决定,这题还不需要回溯具体选了谁。还是按照之前的解题方法来做:

  1. 确定要优化的量

    不必说,就是 strs 的最大子集的长度。

  2. 定义状态

    我们创建一个静态三维数组,来保存状态,和我们 0/1 背包的创建类似:

    static int[][][] dp = new int[strs.length + 1][m + 1][n + 1];

    比如 dp[i][j][k]的值的意思是,截止到 str 中的第 i 个字符串,可以得到最多的的字符串数量。那么,题目所求的,就是 dp[strs.length][m][n]

  3. 写状态转移方程

    依旧十分关键!同样把把问题拆分成不选的两种选择,而若数量超过了限制,也只能不选!

    • 不选

      如果选择不选的话,表达式很好写:

      dp[i][j][k] = dp[i-1][j][k]

      和 0/1 背包的逻辑一模一样!而且也是自底向上的。

    • 如果选择选的话,那么对应的 dp[i][j][k] 又是什么呢?我们的想法依旧是,若我们选了,那么该值应该是 没加这选它之前的价值 + 此物的价值(此物的价值永远是1,因为就是选与不选)

      其实就是:

      dp[i][j][k] = dp[i - 1][j - countZero][k - countOne] + 1

      我们建立一个方法 f ,就是输入一个字符串,返回一个数组 arr,arr 第 0 个坐标为该字符串的 0 的数量,arr 第 1 个坐标为该字符串的 1 的数量。增加 0 的数量为:countZero = arr[0]。增加 1 的数量为:countOne = arr[1]。

      注意 dp[i - 1][j - countZero][k - countOne] 就是 没选之前的价值

      1 就是 这件物品的价值

    和之前一样,还要在做了选与不选的选择之后,我们就要比较,取两者之中最大者

    dp[i][j][k] = Math.max(dp[i - 1][j - countZero][k - countOne] + 1, dp[i - 1][j][k]);
  4. 确定边界值

    边界值其实都是 0 ,我们的数组都是会默认赋值 0 ,所以不需要额外去赋值。

  5. 选择计算顺序

    也是是自底向上的,就不多说了。

最后看我程序:

package dp;


/**
* @Author: sangui
* @CreateTime: 2025-11-11
* @Description:
* @Version: 1.0
*/
public class Q_474 {
static int[][][] dp;

static void main() {
String[] strs = {"10", "0001", "111001", "1", "0"};
int m = 5;
int n = 3;
int res = findMaxForm(strs, m, n);
System.out.println(res);
}

public static int findMaxForm(String[] strs, int m, int n) {
dp = new int[strs.length + 1][m + 1][n + 1];
// 依次遍历每一个数组中的元素
for (int i = 1; i <= strs.length; i++) {
int[] arr = f(strs[i - 1]);
int countZero = arr[0];
int countOne = arr[1];
// m 代表 0 的容量,依次遍历
for (int j = 0; j <= m; j++) {
// n 代表 1 的容量,依次遍历
for (int k = 0; k <= n; k++) {
if (j < countZero || k < countOne) {
// 走到这里,说明背包没有空闲,不可放
dp[i][j][k] = dp[i - 1][j][k];
} else {
// 走到这里,说明背包有空闲,可放,可不放,需要比较这两个值
dp[i][j][k] = Math.max(dp[i - 1][j - countZero][k - countOne] + 1, dp[i - 1][j][k]);
}
}
}
}
return dp[strs.length][m][n];
}

public static int[] f(String str) {
int[] res = new int[2];
int length = str.length();
for (int i = 0; i < length; i++) {
res[str.charAt(i) - '0']++;
}
return res;
}
}

基本想玩五个步骤之后,程序按照之前 0/1 背包的架构,就可以写出来了。接下来就可以实现更新优化了,就是进行二维迭代,现在的空间复杂度是 O(len×m×n)

,可以根据之前我 0/1 背包的思路,将这个三维复杂度,降至二维。哎,今天不知不觉就学算法学到了晚上快九点,有点太累了,以后有时间再优化吧。

最后,提供一下我提出的进阶版爬楼梯题,我的解法,解法如下:

/**
* 每次可以爬 1、2 或 3 阶的爬楼梯的方法(下到上)
*
* @param n 代表 n 阶台阶
* @return 爬 n 阶台阶的有多少种不同的方式
*/
public static int climbStairs4(int n) {
// 1 阶台阶有一种走法
arr[1] = 1;

// 2 阶台阶有两种走法
arr[2] = 2;

// 3 阶台阶有三种走法
arr[3] = 3;

// 第 n 阶台阶(n>3),一定是从 n-1 阶台阶和 n-2 阶台阶和 n-3 阶台阶走来的
// 那么走第 n 阶台阶的走法,就是 n-1 阶台阶的走法加上 n-2 阶台阶的走法
for (int i = 4; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2] + arr[i - 3];
}
return arr[n];
}

/**
* 每次可以爬 1、2 或 3 阶的爬楼梯的方法(上到下)
*
* @param n 代表 n 阶台阶
* @return 爬 n 阶台阶的有多少种不同的方式
*/
public static int climbStairs5(int n) {
if (n == 1) {
// 1 阶台阶有一种走法
arr[n] = 1;
return arr[n];
}
if (n == 2) {
// 2 阶台阶有两种走法
arr[n] = 2;
return arr[n];
}
if (n == 3) {
// 3 阶台阶有三种走法
arr[n] = 3;
return arr[n];
}

if (arr[n] != 0) {
return arr[n];
}

// 第 n 阶台阶(n>4),一定是从 n-1 阶台阶和 n-2 阶台阶和 n-3 阶台阶走来的
// 那么走第 n 阶台阶的走法,就是 n-1 阶台阶的走法加上 n-2 阶台阶,加上 n-3 阶台阶的走法
arr[n] = climbStairs5(n - 1) + climbStairs5(n - 2) + climbStairs5(n - 3);

return arr[n];
}

/**
* 每次可以爬 1、2 或 3 阶楼梯的方法(邪修,无需数组存储)
*
* @param n 代表 n 阶台阶
* @return 爬 n 阶台阶的有多少种不同的方式
*/
public static int climbStairs6(int n) {
// 第 n 阶台阶(n>2),一定是从 n-1 阶台阶和 n-2 阶台阶走来的
// 那么走第 n 阶台阶的走法,就是 n-1 阶台阶的走法加上 n-2 阶台阶的走法

int a = 1;
if (n == 1) {
return a;
}
int b = 2;
if (n == 2) {
return b;
}
int c = 3;
if (n == 3) {
return c;
}
int d = 0;
for (int i = 4; i <= n; i++) {
d = a + b + c;
a = b;
b = c;
c = d;
}
return c;
}
  • 微信
  • 赶快加我聊天吧
  • QQ
  • 赶快加我聊天吧
  • weinxin
三桂

发表评论 取消回复 您未登录,登录后才能评论,前往登录