1 题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
1 <= n <= 45
2 代码实现
c++
cpp
class Solution {
public:
int climbStairs(int n) {
if (n == 1 ) return 1 ;
int dp[46] ;
dp[1] = 1;
dp[2] = 2 ;
for (int i = 3 ; i <= n ; i++){
dp[i] = dp[i - 1] + dp[i -2 ];
}
return dp[n];
}
};
js
javascript
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
if ( n === 1 ) return 1 ;
if ( n === 2 ) return 2 ;
let a = 1 ;
let b = 2 ;
let res = 0 ;
for(let i = 3 ; i <= n ; i++){
res = a + b ;
a = b ;
b = res ;
}
return res ;
};
思考
学习DP重要性极大,我必须学一学DP了,但是我现在还不会,而且我也没看懂之前的动态规划解题套路框架。。。
题解
先看题目
爬 n 阶楼梯,每次只能爬 1 步 或 2 步,问有多少种方法爬到楼顶?
第一步:找规律(动态规划的核心:拆分子问题)
我们先手动算几个小例子,找规律:
- n=1:只能爬 1 步 → 1 种
- n=2:1+1 / 2 → 2 种
- n=3:最后一步只有两种可能:
- 从第 2 阶爬 1 步上来
- 从第 1 阶爬 2 步上来所以:方法数 = 到 2 阶的方法 + 到 1 阶的方法 = 2+1=3 种
- n=4:同理 = 到 3 阶方法 + 到 2 阶方法 = 3+2=5 种
✅ 结论 :爬到第 n 阶的方法数 = 爬到 n-1 阶的方法数 + 爬到 n-2 阶的方法数 这就是递推公式,动态规划的灵魂!
第二步:动态规划三要素(背下来,所有 DP 题都用)
- dp 数组定义 :
dp[i]= 爬到第i阶的方法数 - 递推公式 :
dp[i] = dp[i-1] + dp[i-2] - 初始化 :最开始的基础值
dp[1] = 1dp[2] = 2
版本 1:基础版(数组实现,最好理解)
cpp
#include <iostream>
using namespace std;
int climbStairs(int n) {
// 特殊情况:n=1 直接返回1
if (n == 1) return 1;
// 1. 定义dp数组:dp[i] 表示爬到第i阶的方法数
int dp[46]; // 题目n最大45
// 2. 初始化(基础值)
dp[1] = 1;
dp[2] = 2;
// 3. 循环计算:从3到n,用递推公式
for (int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
// 返回结果
return dp[n];
}
int main() {
int n;
cin >> n;
cout << climbStairs(n) << endl;
return 0;
}
版本 2:空间优化版(不用数组,省内存)
观察发现:我们只需要前两个数,不需要存整个数组!
cpp
#include <iostream>
using namespace std;
int climbStairs(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
// 用两个变量保存前两个值
int a = 1; // dp[i-2]
int b = 2; // dp[i-1]
int res;
for (int i = 3; i <= n; i++) {
res = a + b; // 当前值 = 前两个之和
a = b; // 滚动更新:a 变成原来的 b
b = res; // b 变成当前值
}
return res;
}
int main() {
int n;
cin >> n;
cout << climbStairs(n) << endl;
return 0;
}
版本 3:极简递归版(好理解,但不推荐,会重复计算)
cpp
int climbStairs(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
// 直接用递推公式递归
return climbStairs(n-1) + climbStairs(n-2);
}
⚠️ 缺点:n 大了会超时,重复计算太多,正式做题用前两个迭代版本。
第四步:动态规划到底是什么?(一句话总结)
动态规划就是:把大问题拆成小问题 → 找到小问题的规律(递推公式)→ 用小问题的答案算出大问题的答案
它的核心就是不重复计算,把之前算过的结果存起来,直接用。
第五步:验证一下(和题目示例完全匹配)
- 输入 n=2 → 输出 2 ✔️
- 输入 n=3 → 输出 3 ✔️
- 输入 n=4 → 输出 5 ✔️
- 输入 n=5 → 输出 8 ✔️
总结
- 爬楼梯本质就是斐波那契数列
- 动态规划三步:
- 定义
dp[i] - 写递推公式
- 初始化 + 循环计算
- 定义
- 代码优先用优化迭代版,效率最高
3 题目
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 1000 <= nums[i] <= 400
4 代码实现
c++
cpp
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0 ) return 0 ;
if (nums.size() == 1 ) return nums[0] ;
vector<int> dp(nums.size());
dp[0] = nums[0];
dp[1] = max (nums[0] , nums[1]);
for (int i = 2 ; i < nums.size() ; i++){
dp[i] = max(dp[i - 1], dp[i - 2]+ nums[i]);
}
return dp.back();
}
};
js
javascript
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
if(nums.length == 0) return 0 ;
let prev2 = 0 ;
let prev1 = 0 ;
for (const num of nums){
let temp = prev1;
prev1 = prev1 > (prev2 + num ) ? prev1 : (prev2 + num);
prev2 = temp ;
}
return prev1 ;
};
思考
这咋办?这又不是斐波那契数列,说实话这道题换成数学题我也都不会啊。。我就知道一个最大金额应该是要动态维护的。。然后呢?
题解
先把题目翻译成人话
你是小偷,有一排房子:
- 不能偷相邻的
- 问:最多能偷多少钱?
就这么简单!
第一步:用小偷思维找规律(超级关键)
我们从最后一间房子开始想(动态规划的核心:看最后一步怎么做)
假设你走到第 i 间房子,你只有两个选择:
-
偷这间 那你前一间(i-1)绝对不能偷总钱 = 偷到 i-2 时的最大钱 + 这间房子的钱
-
不偷这间那总钱 = 偷到 i-1 时的最大钱
最终答案 = 两个选择里钱更多的那个!
第二步:动态规划三要素(直接套)
我直接给你写好,你背住就能做题!
1. dp 数组定义
dp[i] = 偷到第 i 间房子 时,能偷到的最大金额
2. 递推公式(灵魂)
cpp
dp[i] = max( 不偷i , 偷i )
dp[i] = max( dp[i-1], dp[i-2] + nums[i] )
dp[i-1]→ 不偷第 i 间dp[i-2] + nums[i]→ 偷第 i 间
3. 初始化
- 只有 1 间房:
dp[0] = nums[0] - 只有 2 间房:
dp[1] = max(nums[0], nums[1])
第三步:直接看代码(逐行注释,C++)
版本 1:最容易理解的数组版
cpp
#include <iostream>
#include <vector>
#include <algorithm> // 要用 max 函数
using namespace std;
int rob(vector<int>& nums) {
// 边界情况
if (nums.size() == 0) return 0;
if (nums.size() == 1) return nums[0];
// 1. 创建 dp 数组
vector<int> dp(nums.size());
// 2. 初始化
dp[0] = nums[0]; // 第一间:只能偷它
dp[1] = max(nums[0], nums[1]); // 前两间:偷钱多的那个
// 3. 从第三间开始递推
for (int i = 2; i < nums.size(); i++) {
// 核心公式!
dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
}
// 最后一间就是答案
return dp.back();
}
int main() {
vector<int> nums = {1,2,3,1};
cout << rob(nums) << endl; // 输出 4,正确!
return 0;
}
版本 2:空间优化版(不用数组,面试首选)
我们只需要前两个值,不用存整个数组:
cpp
int rob(vector<int>& nums) {
if(nums.empty()) return 0;
int prev2 = 0; // dp[i-2]
int prev1 = 0; // dp[i-1]
for(int num : nums){
int temp = prev1;
// 当前最大值 = max(不偷, 偷)
prev1 = max(prev1, prev2 + num);
prev2 = temp;
}
return prev1;
}
第四步:带你手动算一遍(立刻懂!)
示例 [1,2,3,1]
- dp[0] = 1
- dp[1] = max(1,2) = 2
- dp[2] = max(dp[1]=2, dp[0]+3=4) → 4
- dp[3] = max(dp[2]=4, dp[1]+1=3) → 4
最终答案:4 ✔️
完美和题目一致!
第五步:你最关心的问题
"这不是斐波那契,我怎么想到递推公式?"
我给你一个万能套路,所有动态规划都能用:
动态规划思考三步法(背下来!)
-
看最后一步:最后一个位置能干嘛?
- 爬楼梯:最后一步走 1 或 2
- 打家劫舍:最后一间偷或不偷
-
写出两种选择
-
取最优(最大 / 最小)
只要你学会看 "最后一步",所有 DP 题你都会了!
超级总结(你只需要记这 3 句)
- 偷 = 前前一个 + 当前钱
- 不偷 = 前一个的钱
- 选最大的那个!
公式:
javascript
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
5 小结
感觉动态规划也不是很难,有点像高中做的递归数列,我的天,我想到以前还有特征方程和特征根,简直就是上辈子的记忆一样哈哈哈。
好,其实动态规划的思想就是类似斐波那契数列的。把在此之前的情况分为两种,爬1or2级台阶,或者偷 or 不偷。
多加练习自己再好好体会。
通法:
1. 定义 dp [i]
dp [i] = 到第 i 个位置时的答案
2. 看最后一步有几种选择
永远只有 2 种选择(99% 的题)
3. 写出递推公式
- 求数量 → 相加
- 求最大 / 最小 → 用 max /min
4. 初始化前 1~2 个值
然后循环从 3 开始算到最后。