Leetcode100: 70.爬楼梯、118.杨辉三角、198.打家劫舍

题目1:爬楼梯(LeetCode 70)

问题描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例:

复制代码
输入: n = 3
输出: 3
解释: 有三种方法可以爬到楼顶
1. 1阶 + 1阶 + 1阶
2. 1阶 + 2阶
3. 2阶 + 1阶

解题思路

核心思想: 动态规划(斐波那契数列)

  • 到达第 n 阶的方法数 = 到达第 n-1 阶的方法数 + 到达第 n-2 阶的方法数

  • 因为最后一步要么走1阶,要么走2阶

  • 状态转移方程:dp[n] = dp[n-1] + dp[n-2]

  • 初始条件:dp[1] = 1(走1阶),dp[2] = 2(1+1 或 2)

Java代码(带详细注释)

java 复制代码
/**
 * LeetCode 70. 爬楼梯
 * 难度:简单
 * 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
 * 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
 */
public class ClimbingStairs {
    /**
     * 解法1:动态规划(使用数组)
     * 时间复杂度:O(n),空间复杂度:O(n)
     */
    public int climbStairs(int n) {
        // 边界检查
        if (n <= 2) {
            return n;
        }
        
        // dp[i] 表示到达第 i 阶的方法数
        int[] dp = new int[n + 1];
        dp[1] = 1;  // 1阶:只有1种方法(走1步)
        dp[2] = 2;  // 2阶:有2种方法(1+1 或 2)
        
        // 从第3阶开始计算
        for (int i = 3; i <= n; i++) {
            // 到达第i阶的方法 = 到达第i-1阶的方法 + 到达第i-2阶的方法
            // 因为最后一步可以是1阶或2阶
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        
        return dp[n];
    }
    
    /**
     * 解法2:滚动数组优化(空间优化)
     * 时间复杂度:O(n),空间复杂度:O(1)
     * 因为 dp[i] 只依赖于 dp[i-1] 和 dp[i-2],所以只需要两个变量
     */
    public int climbStairsOptimized(int n) {
        if (n <= 2) {
            return n;
        }
        
        int prev2 = 1;  // dp[i-2],初始为dp[1]
        int prev1 = 2;  // dp[i-1],初始为dp[2]
        int current = 0;
        
        for (int i = 3; i <= n; i++) {
            current = prev1 + prev2;  // dp[i] = dp[i-1] + dp[i-2]
            prev2 = prev1;             // 滚动:dp[i-2] = dp[i-1]
            prev1 = current;           // 滚动:dp[i-1] = dp[i]
        }
        
        return current;
    }
}

流程图(Mermaid)

执行过程图解:

复制代码
n = 5

初始化:
dp[1] = 1  → 第1阶:1种方法
dp[2] = 2  → 第2阶:2种方法

计算:
i=3: dp[3] = dp[2] + dp[1] = 2 + 1 = 3
     方法:111, 12, 21

i=4: dp[4] = dp[3] + dp[2] = 3 + 2 = 5
     方法:1111, 112, 121, 211, 22

i=5: dp[5] = dp[4] + dp[3] = 5 + 3 = 8
     方法:8种

最终答案: 8 ✓

题目2:杨辉三角(LeetCode 118)

问题描述

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

示例:

复制代码
输入: numRows = 5
输出:
[
     [1],
    [1,1],
   [1,2,1],
  [1,3,3,1],
 [1,4,6,4,1]
]

解题思路

核心思想: 动态规划(逐行构建)

  • 每一行的第一个和最后一个元素都是 1

  • 中间元素 = 上一行相邻两个元素之和

  • 状态转移:dp[i][j] = dp[i-1][j-1] + dp[i-1][j]

Java代码(带详细注释)

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * LeetCode 118. 杨辉三角
 * 难度:简单
 * 给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
 */
public class PascalsTriangle {
    public List<List<Integer>> generate(int numRows) {
        // 创建结果列表
        List<List<Integer>> result = new ArrayList<>();
        
        // 边界检查
        if (numRows <= 0) {
            return result;
        }
        
        // 逐行构建
        for (int i = 0; i < numRows; i++) {
            // 创建当前行
            List<Integer> row = new ArrayList<>();
            
            // 每一行的第一个元素是 1
            row.add(1);
            
            // 计算中间元素(从第2行开始)
            if (i > 0) {
                // 获取上一行
                List<Integer> prevRow = result.get(i - 1);
                
                // 中间元素 = 上一行相邻两个元素之和
                for (int j = 1; j < i; j++) {
                    int sum = prevRow.get(j - 1) + prevRow.get(j);
                    row.add(sum);
                }
            }
            
            // 每一行的最后一个元素是 1(第1行只有一个1)
            if (i > 0) {
                row.add(1);
            }
            
            // 将当前行加入结果
            result.add(row);
        }
        
        return result;
    }
}

流程图(Mermaid)

执行过程图解:

复制代码
numRows = 5

第1行 (i=0):
row = [1]
result = [[1]]

第2行 (i=1):
row = [1]
prevRow = [1]
中间元素: j从1到0,没有中间元素
row.add(1) → row = [1, 1]
result = [[1], [1, 1]]

第3行 (i=2):
row = [1]
prevRow = [1, 1]
中间元素:
  j=1: sum = prevRow[0] + prevRow[1] = 1 + 1 = 2
  row = [1, 2]
row.add(1) → row = [1, 2, 1]
result = [[1], [1, 1], [1, 2, 1]]

第4行 (i=3):
row = [1]
prevRow = [1, 2, 1]
中间元素:
  j=1: sum = prevRow[0] + prevRow[1] = 1 + 2 = 3
  j=2: sum = prevRow[1] + prevRow[2] = 2 + 1 = 3
  row = [1, 3, 3]
row.add(1) → row = [1, 3, 3, 1]
result = [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]]

第5行 (i=4):
row = [1]
prevRow = [1, 3, 3, 1]
中间元素:
  j=1: sum = 1 + 3 = 4
  j=2: sum = 3 + 3 = 6
  j=3: sum = 3 + 1 = 4
  row = [1, 4, 6, 4]
row.add(1) → row = [1, 4, 6, 4, 1]
result = [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]

最终结果: ✓

杨辉三角的数学性质:

复制代码
        1         ← 第0行 (2^0)
       1 1        ← 第1行 (2^1)
      1 2 1       ← 第2行 (2^2)
     1 3 3 1      ← 第3行 (2^3)
    1 4 6 4 1     ← 第4行 (2^4)

1. 每行数字之和 = 2^(行号)
2. 每行是 (a+b)^n 的展开系数
3. 每个数 = 组合数 C(n, k)

题目3:打家劫舍(LeetCode 198)

问题描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金。相邻的房屋不能同时偷窃 ,否则会触发报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报的情况下,一夜之内能够偷窃到的最高金额。

示例:

复制代码
输入: [1, 2, 3, 1]
输出: 4
解释: 偷窃 1号房屋 (金额 = 1) + 3号房屋 (金额 = 3) = 4
      不能偷窃 2号 (2) + 3号 (3),因为相邻

解题思路

核心思想: 动态规划

  • 状态定义:dp[i] 表示偷窃前 i 个房屋能获得的最大金额

  • 状态转移:对于第 i 个房屋,有两种选择:

    1. 不偷dp[i-1](和偷前 i-1 个一样)

    2. dp[i-2] + nums[i-1](偷当前 + 前 i-2 个的最优值)

  • 状态转移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i-1])

Java代码(带详细注释)

java 复制代码
/**
 * LeetCode 198. 打家劫舍
 * 难度:中等
 * 你是一个专业的小偷,计划偷窃沿街的房屋。相邻的房屋不能同时偷窃。
 * 计算不触动警报的情况下,能够偷窃到的最高金额。
 */
public class HouseRobber {
    /**
     * 解法1:动态规划(使用数组)
     * 时间复杂度:O(n),空间复杂度:O(n)
     */
    public int rob(int[] nums) {
        // 边界检查
        if (nums == null || nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return nums[0];
        }
        
        int n = nums.length;
        // dp[i] 表示偷窃前 i 个房屋的最大金额
        int[] dp = new int[n + 1];
        dp[0] = 0;              // 没有房屋,金额为0
        dp[1] = nums[0];        // 只有1个房屋,只能偷它
        
        // 从第2个房屋开始计算
        for (int i = 2; i <= n; i++) {
            // 对于第i个房屋(索引i-1):
            // 1. 不偷:dp[i-1]
            // 2. 偷:dp[i-2] + nums[i-1]
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
        }
        
        return dp[n];
    }
    
    /**
     * 解法2:滚动数组优化(空间优化)
     * 时间复杂度:O(n),空间复杂度:O(1)
     */
    public int robOptimized(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        if (nums.length == 1) {
            return nums[0];
        }
        
        int prev2 = 0;        // dp[i-2],初始为dp[0]
        int prev1 = nums[0];  // dp[i-1],初始为dp[1]
        int current = 0;
        
        for (int i = 2; i <= nums.length; i++) {
            // dp[i] = max(dp[i-1], dp[i-2] + nums[i-1])
            current = Math.max(prev1, prev2 + nums[i - 1]);
            prev2 = prev1;
            prev1 = current;
        }
        
        return prev1;
    }
}

流程图(Mermaid)

执行过程图解:

复制代码
nums = [1, 2, 3, 1]

初始化:
dp[0] = 0  → 没有房屋
dp[1] = 1  → 只偷第1个房屋

计算:
i=2(偷前2个房屋):
  不偷第2个:dp[1] = 1
  偷第2个:dp[0] + nums[1] = 0 + 2 = 2
  dp[2] = max(1, 2) = 2
  决策:偷第2个(2)

i=3(偷前3个房屋):
  不偷第3个:dp[2] = 2
  偷第3个:dp[1] + nums[2] = 1 + 3 = 4
  dp[3] = max(2, 4) = 4
  决策:偷第1个+第3个(1+3=4)

i=4(偷前4个房屋):
  不偷第4个:dp[3] = 4
  偷第4个:dp[2] + nums[3] = 2 + 1 = 3
  dp[4] = max(4, 3) = 4
  决策:偷第1个+第3个(1+3=4)

最终答案: 4 ✓

决策过程可视化:

复制代码
房屋: [1, 2, 3, 1]
       ↓  ↓  ↓  ↓

方案1: 偷 1, 3 → 1 + 3 = 4 ✓
方案2: 偷 2, 4 → 2 + 1 = 3
方案3: 偷 1, 4 → 1 + 1 = 2
方案4: 偷 2    → 2
方案5: 偷 3    → 3

最优: 4 ✓

问题1:为什么是 new int[n + 1],不能是 new int[n]

答: 因为 dp[i] 表示偷窃前 i 个 房屋的最大金额,i 的范围是 0n

核心理解:

java 复制代码
int n = nums.length;  // 房屋数量

// dp[i] 表示:偷窃前 i 个房屋(索引 0 到 i-1)的最大金额
int[] dp = new int[n + 1];
// 索引范围:0, 1, 2, ..., n
//           ↑              ↑
//         没有房屋      所有房屋

// 如果使用 new int[n]:
int[] dp = new int[n];
// 索引范围:0, 1, 2, ..., n-1
// 问题:dp[n] 表示偷窃所有房屋,但 n 超出了范围!

详细对比:

房屋数 数组长度 可用索引 能否表示"所有房屋"
n = 3 n = 3 0, 1, 2 ❌ 不能(缺少索引3)
n = 3 n+1 = 4 0, 1, 2, 3 ✅ 可以(索引3)

为什么需要 dp[0] = 0

java 复制代码
// dp[0] 表示"没有房屋"的情况
dp[0] = 0;  // 没有房屋可偷,金额为0

// dp[1] 表示"只有第1个房屋"的情况
dp[1] = nums[0];  // 只能偷这一个

// 这样定义的好处:
// 当 i=2 时,dp[2] = max(dp[1], dp[0] + nums[1])
//                              ↑       ↑
//                       不偷第2个   偷第2个

如果使用 new int[n],问题出在哪?

java 复制代码
// 错误示例:使用 new int[n]
int[] dp = new int[n];  // 假设 n=3,索引 0, 1, 2

dp[0] = 0;   // 没有房屋?但索引0表示第1个房屋,冲突!
dp[1] = nums[0];  // 索引1表示第2个房屋?混乱!

// 问题:索引含义不清晰
// dp[0] 到底表示"没有房屋"还是"第1个房屋"?
// dp[2] 表示"第3个房屋"还是"前3个房屋"?

// 会导致代码混乱和错误!

问题2:会不会有两个很大的数,但是不能同时选,跳过两个房子就能选了,并且是最优解?

答: 这个问题太棒了!这正好是 DP 设计的核心 ------ DP 自动处理了这种"隔两个房子"的情况!

假设场景:

复制代码
房屋: [5, 1, 1, 5]
索引:  0  1  2  3

不能偷相邻:
- 选择 5(0) + 5(3) = 10 ✓(隔了两个房屋)
- 选择 5(0) + 1(2) = 6
- 选择 1(1) + 5(3) = 6
- 选择 5(0) + 1(1) = ❌ 相邻
- 选择 1(2) + 5(3) = ❌ 相邻

最优解:10(偷索引0和索引3)

DP 如何自动处理这种情况?

复制代码
nums = [5, 1, 1, 5]
n = 4

dp[0] = 0
dp[1] = 5  // 偷第1个:5

i=2:
  不偷第2个:dp[1] = 5
  偷第2个:dp[0] + nums[1] = 0 + 1 = 1
  dp[2] = max(5, 1) = 5  // 决策:不偷第2个

i=3:
  不偷第3个:dp[2] = 5
  偷第3个:dp[1] + nums[2] = 5 + 1 = 6  ← 注意!这里跳到 dp[1]
  dp[3] = max(5, 6) = 6  // 决策:偷第1个 + 第3个(5+1=6)

i=4:
  不偷第4个:dp[3] = 6
  偷第4个:dp[2] + nums[3] = 5 + 5 = 10  ← 关键!跳到 dp[2]
  dp[4] = max(6, 10) = 10  // 决策:偷第1个 + 第4个(5+5=10)

最终答案:10 ✓

关键点:dp[i-2] 跳过了两个房屋!

复制代码
当计算 dp[4] 时:
偷第4个房屋 → dp[2] + nums[3]
              ↑
          前2个房屋(索引0,1)的最优解

dp[2] = 5 意味着:偷索引0(5)的决策

所以:dp[2] + nums[3] = 5 + 5 = 10
      = 偷第1个房屋(5) + 偷第4个房屋(5)
      = 跳过了第2、3个房屋!✓

图解跳两个房屋:

复制代码
房屋: [5, 1, 1, 5]

dp[4] 的计算路径:
dp[4] = max(不偷4, 偷4)
     = max(dp[3], dp[2] + 5)
              ↑       ↑
          不偷4    偷4时要跳过一个

dp[2] 的计算路径:
dp[2] = max(不偷2, 偷2)
     = max(dp[1], dp[0] + 1)
              ↑       ↑
          不偷2    偷2时要跳过一个

dp[1] = 5(偷第1个)

完整决策链:
偷第4个(5) ← dp[2] ← 不偷第2个 ← dp[1] ← 偷第1个(5)
                          ↑
                      跳过了第2个
但第3个呢?从 dp[2] 到 dp[4] 跳过了第3个!

结果:偷第1个(5) + 偷第4个(5) = 10 ✓

更多例子:

复制代码
// 场景1:间隔两个房屋
nums = [100, 1, 1, 100, 100]
        ↑           ↑
     最优方案:100 + 100 = 200

dp计算:
dp[0] = 0
dp[1] = 100
dp[2] = max(100, 1) = 100
dp[3] = max(dp[2]=100, dp[1]+1=101) = 101  // 偷0+2
dp[4] = max(dp[3]=101, dp[2]+100=200) = 200 // 偷0+4 ✓

// 场景2:间隔三个房屋
nums = [100, 1, 1, 1, 100]
        ↑              ↑
     最优方案:100 + 100 = 200

dp计算:
dp[0] = 0
dp[1] = 100
dp[2] = 100
dp[3] = 100
dp[4] = max(dp[3]=100, dp[2]+1=101) = 101
dp[5] = max(dp[4]=101, dp[3]+100=200) = 200  // 偷0+4 ✓

总结:DP 如何自动处理"跳过两个房子":

复制代码
dp[i] = max(dp[i-1], dp[i-2] + nums[i-1])
             ↑         ↑
          不偷当前   偷当前(自动跳过一个)

当偷当前房屋时,使用 dp[i-2],这意味着:
- 已经考虑了前 i-2 个房屋的最优解
- 这个最优解可能已经跳过了某些房屋
- 也可能包含了许多间隔的房屋

所以,dp 通过状态转移,自动计算了所有可能的跳过方案!

关键理解:

复制代码
dp[i] 不是一个"选择方案",而是"最优金额"

dp[i] 本身已经包含了:
✅ 跳过第1个房屋
✅ 跳过第2个房屋
✅ 跳过中间某个房屋
✅ 所有可能的跳过组合的最优解

这就是动态规划的威力:将复杂问题分解为子问题,子问题的最优解自动组合成大问题的最优解!

扩展思考:为什么 DP 能处理"任意间隔"?

复制代码
// 打家劫舍的本质是"不选相邻元素的最大子序列和"

房屋: [a0, a1, a2, a3, a4, a5]

dp[5] 的决策树:
          dp[5]
         /     \
    不偷5    偷5
      ↓        ↓
    dp[4]   dp[3] + a4
            /     \
        不偷4    偷4
          ↓        ↓
        dp[3]   dp[2] + a3
        /   \     /   \
      ...   ...  ...  ...

这棵树穷举了所有"不相邻"的组合!
虽然我们只计算了 dp[5],但它实际上探索了:
- 偷5 → 不能偷4 → 考虑3
- 偷5 → 不能偷4 → 不偷3 → 考虑2
- 不偷5 → 考虑4 → 偷4 → 不能偷3 → 考虑2
- ... 所有情况!

dp 通过 memoization(记忆化)避免了重复计算,但保证了考虑所有情况!
相关推荐
随意起个昵称1 小时前
状压dp-基础题目2([USACO12MAR] Cows in a Skyscraper G)
c++·算法·动态规划
这不小天嘛1 小时前
JAVA八股——redis篇
java·开发语言·redis
Jerry2 小时前
LeetCode 1002. 查找共用字符
算法
无限的鲜花2 小时前
协程本质是函数加状态机——零基础深入浅出 C++20 协程
c++·算法·c++20
旖-旎3 小时前
《LeetCode 64 最小路径和 || LeetCode 174 地下城游戏》
c++·算法·leetcode·动态规划
森林古猿13 小时前
再论斜率优化
c++·学习·算法
万法若空3 小时前
【数学-简单数论】同余中的逆
线性代数·算法
聚美智数3 小时前
黄历查询-运势查询-国际法定节假日查询-API接口介绍
android·java·数据库
凯瑟琳.奥古斯特4 小时前
力扣1009补码解法C++实现
开发语言·c++·算法·leetcode·职场和发展