- 使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
提示:
2 <= cost.length <= 1000
0 <= cost[i] <= 999
分析
我们同样可以用动态规划来解决这道「最小花费爬楼梯」问题。设 n = cost.size()
,我们要到达的“楼顶”可以看作下标 n
(此处没有实际的 cost,需要付费即可到达终点)。
思路
-
定义
dp[i]
为到达位置i
时的最小累计花费。 -
因为每次可以爬 1 或 2 阶:
- 从
i-1
跨一步到i
,需付费cost[i-1]
; - 从
i-2
跨两步到i
,需付费cost[i-2]
;
- 从
-
状态转移:
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
-
边界(起始):
dp[0] = 0
:还没开始爬,花费 0;dp[1] = 0
:可以从下标 1 开始,也不需要先付费就到达 1。
最终答案为 dp[n]
。
注意:这样定义的好处是最后一步到达 n
时,并不需要为下标 n
支付任何 cost
,因为 cost
只有 0…n−1。
方法一:O(n) 时间 + O(n) 空间
// C++ 实现
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n+1, 0);
// dp[0]=0, dp[1]=0 已默认
for (int i = 2; i <= n; ++i) {
dp[i] = min(dp[i-1] + cost[i-1],
dp[i-2] + cost[i-2]);
}
return dp[n];
}
};
# Python 实现
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0] * (n + 1)
# dp[0]=0, dp[1]=0
for i in range(2, n + 1):
dp[i] = min(dp[i-1] + cost[i-1],
dp[i-2] + cost[i-2])
return dp[n]
- 时间复杂度:O(n)
- 空间复杂度:O(n)
方法二:O(n) 时间 + O(1) 空间(滚动变量)
我们注意到 dp[i]
只依赖于 dp[i-1]
和 dp[i-2]
,因此可以只用两个变量滚动维护:
// C++ 实现(O(1) 空间)
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
int prev2 = 0; // dp[i-2]
int prev1 = 0; // dp[i-1]
int cur;
for (int i = 2; i <= n; ++i) {
cur = min(prev1 + cost[i-1],
prev2 + cost[i-2]);
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
};
# Python 实现(O(1) 空间)
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
prev2, prev1 = 0, 0 # dp[i-2], dp[i-1]
for i in range(2, n + 1):
cur = min(prev1 + cost[i-1],
prev2 + cost[i-2])
prev2, prev1 = prev1, cur
return prev1
- 时间复杂度:O(n)
- 空间复杂度:O(1)
示例验证
-
示例 1:
cost = [10,15,20]
dp[2] = min(dp[1]+cost[1], dp[0]+cost[0]) = min(0+15, 0+10) = 10 dp[3] = min(dp[2]+cost[2], dp[1]+cost[1]) = min(10+20, 0+15) = 15 返回 dp[3]=15
-
示例 2:
cost = [1,100,1,1,1,100,1,1,100,1]
亦可按上述方法逐步计算,最终得到 6。
以上两种方法都能在线性时间内求解,其中滚动变量的方法在空间上更优。
花费
这两个 cost
数组只是用来举例说明「最小花费爬楼梯」的输入格式——数组下标代表台阶编号,数组值代表踏上该台阶需要支付的费用。下面我逐一拆解这两个例子,帮助你看懂它们的设定和最优策略是如何得出的。
例子一:cost = [10, 15, 20]
-
共有 3 个台阶,编号 0、1、2,对应的费用分别是
cost[0] = 10, cost[1] = 15, cost[2] = 20
-
“楼顶”可以看作是下标 3(超出数组末尾,不需要支付费用)。
-
可以从台阶 0 或 台阶 1 开始,不用先付 0 号或 1 号之前的费用。
所有可能的起点和路径
-
从 0 开始:
- 选一步到 1(付 15),再一步到 2(付 20),再一步到顶(不付费)→ 总花费 15+20 = 35
- 或一步到 1(15),两步到顶(不用再付 20)→ 总花费 15
- 或两步到 2(一次性跨过 0→2,付费 cost[0] = 10),再一步到顶→ 总花费 10
在这三条路径里,最少是 10(先跨两步付10,再跨一步不付费)。
-
从 1 开始:
- 一步到 2(付 20),再一步到顶 → 20
- 两步直接到顶(从 1→3,不需要付 cost[2])→ 付费 cost[1] = 15
最少是 15。
比较从 0 开始最优(10)和从 1 开始最优(15),取更小,答案就是 10?
注意:上面从 0 出发跨两步是付 cost[0],而不是 cost[2],这一点是容易混淆的。
不过官方示例给出的答案是 15,这是因为他们在「从 0 开始」时,默认的最优策略是:
- 付 10(0→2),再 0→3(不用费 cost[2])其实也是可以的,花费 10。
但 LeetCode 示例里他们没列出这条路径,而是直接说「从 1 开始,两步到顶,花费 15」。
实际上,对于[10,15,20]
,最小花费应该是 10。
例子二:
cost = [1,100,1,1,1,100,1,1,100,1]
长度是 10,对应台阶下标 0…9;楼顶是下标 10。
下标 i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
cost[i] | 1 | 100 | 1 | 1 | 1 | 100 | 1 | 1 | 100 | 1 |
最优路径解析
- 最开始我们可以选从 0 或 1 开始;显然
cost[0]=1
比cost[1]=100
小,所以从 0 出发更优。 - 从 0→2:付费 cost[0] = 1
- 从 2→4:付费 cost[2] = 1
- 从 4→6:付费 cost[4] = 1
- 从 6→7:付费 cost[6] = 1
- 从 7→9:付费 cost[7] = 1
- 从 9→10(楼顶):付费 cost[9] = 1
总花费 = 1 + 1 + 1 + 1 + 1 + 1 = 6
你也可以验证:
- 每次都尽量跳过
100
的台阶(下标 1、5、8), - 在 1、2、4、6、7、9 等低费台阶付款。
小结
-
cost[i]
就是「踏上台阶 i 要付的钱」。 -
最后到达的“楼顶”下标是
n = cost.length
,不需要付费。 -
你可以从下标 0 或 1 开始;第一次踏上就是付对应的
cost[起点]
。 -
动态规划的状态转移:
dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
最后返回
dp[n]
。
希望这样分步拆解,能让你看懂这两个示例中 cost
数组的具体含义和最优付费路径!