题目描述
给定一个长度为 n 的下标从 0 开始的整数数组 nums
。初始位置为下标 0
。当 i < j
时,你可以从下标 i
跳转到下标 j
:
- 对于在
i < k < j
范围内的所有下标k
有nums[i] <= nums[j]
和nums[k] < nums[i]
, 或者 - 对于在
i < k < j
范围内的所有下标k
有nums[i] > nums[j]
和nums[k] >= nums[i]
。
你还得到了一个长度为 n
的整数数组 costs
,其中 costs[i]
表示跳转到下标 i
的代价。
返回跳转到下标 n - 1
的最小代价。
示例 1:
输入: nums = [3,2,4,4,1], costs = [3,7,6,4,2]
输出: 8
解释: 从下标 0 开始。
- 以 costs[2]= 6 的代价跳转到下标 2。
- 以 costs[4]= 2 的代价跳转到下标 4。
总代价是 8。可以证明,8 是所需的最小代价。
另外两个可能的路径是:下标 0 -> 1 -> 4 和下标 0 -> 2 -> 3 -> 4。
它们的总代价分别为9和12。
示例 2:
输入: nums = [0,1,2], costs = [1,1,1]
输出: 2
解释: 从下标 0 开始。
- 以 costs[1] = 1 的代价跳转到下标 1。
- 以 costs[2] = 1 的代价跳转到下标 2。
总代价是 2。注意您不能直接从下标 0 跳转到下标 2,因为 nums[0] <= nums[1]。
解释:
n == nums.length == costs.length
1 <= n <= 10^5
0 <= nums[i], costs[i] <= 10^5
问题分析
这道题是跳跃游戏系列中的一道具有挑战性的题目。关键在于理解跳跃条件:
跳跃条件解析:
-
条件一: nums[i] <= nums[j] 且中间所有 nums[k] < nums[i]
-
含义:从位置i跳到右侧第一个 ≥ nums[i] 的位置
-
对应:单调递减栈(维护一个从栈底到栈顶递减的序列)
-
-
条件二: nums[i] > nums[j] 且中间所有 nums[k] >= nums[i]
-
含义:从位置i跳到右侧第一个 < nums[i] 的位置
-
对应:单调递增栈(维护一个从栈底到栈顶递增的序列)
-
这两个条件实际上是互补的,可以用单调栈来高效处理。
解题思路
单调栈 + 动态规划
核心思想:
- 使用两个单调栈分别处理两种跳跃条件
- 用动态规划记录到达每个位置的最小代价
- 从左到右遍历,动态更新可达位置的最小代价
算法步骤:
- 初始化 dp[i] 表示到达位置 i 的最小代价,dp[0] = 0
- 维护两个单调栈:
- stack1:处理条件一(单调递减栈)
- stack2:处理条件二(单调递增栈)
- 对每个位置 j,检查能从哪些位置跳到 j,并更新最小代价
算法过程
输入: nums = [3,2,4,4,1], costs = [3,7,6,4,2]
初始状态
位置: 0 1 2 3 4
nums: [3, 2, 4, 4, 1]
costs: [3, 7, 6, 4, 2]
dp: [0, ∞, ∞, ∞, ∞]stack1: [] (单调递减栈 - 处理条件一)
stack2: [] (单调递增栈 - 处理条件二)
步骤1:处理位置0 (nums[0] = 3)
当前位置 j = 0, nums[0] = 3stack1操作:
- 栈为空,直接入栈
- stack1: [0]stack2操作:
- 栈为空,直接入栈
- stack2: [0]dp状态:[0, ∞, ∞, ∞, ∞]
步骤2:处理位置1 (nums[1] = 2)
当前位置 j = 1, nums[1] = 2stack1操作(条件一):
- nums[stack1.top()] = nums[0] = 3 > nums[1] = 2
- 不满足 nums[i] <= nums[j],不弹出
- stack1: [0, 1]stack2操作(条件二):
- nums[stack2.top()] = nums[0] = 3 > nums[1] = 2 ✓
- 弹出位置0,更新 dp[1] = min(∞, dp[0] + costs[1]) = min(∞, 0 + 7) = 7
- 将位置1入栈
- stack2: [1]dp状态:[0, 7, ∞, ∞, ∞]跳跃路径:0 → 1 (代价7)
步骤3:处理位置2 (nums[2] = 4)
当前位置 j = 2, nums[2] = 4stack1操作(条件一):
- nums[1] = 2 <= nums[2] = 4 ✓,弹出位置1dp[2] = min(∞, dp[1] + costs[2]) = min(∞, 7 + 6) = 13
- nums[0] = 3 <= nums[2] = 4 ✓,弹出位置0 dp[2] = min(13, dp[0] + costs[2]) = min(13, 0 + 6) = 6
- 将位置2入栈
- stack1: [2]stack2操作(条件二):
- nums[1] = 2 < nums[2] = 4,不满足条件二,不弹出
- 将位置2入栈
- stack2: [1, 2]dp状态:[0, 7, 6, ∞, ∞]跳跃路径:0 → 2 (代价6) 或 0 → 1 → 2 (代价13)
最优:0 → 2 (代价6)
步骤4:处理位置3 (nums[3] = 4)
当前位置 j = 3, nums[3] = 4stack1操作(条件一):
- nums[2] = 4 <= nums[3] = 4 ✓,弹出位置2dp[3] = min(∞, dp[2] + costs[3]) = min(∞, 6 + 4) = 10
- 将位置3入栈
- stack1: [3]stack2操作(条件二):
- nums[2] = 4 不大于 nums[3] = 4,不弹出
- 将位置3入栈
- stack2: [1, 2, 3]dp状态:[0, 7, 6, 10, ∞]跳跃路径:0 → 2 → 3 (代价10)
步骤5:处理位置4 (nums[4] = 1)
当前位置 j = 4, nums[4] = 1stack1操作(条件一):
- nums[3] = 4 > nums[4] = 1,不满足条件一,不弹出
- 将位置4入栈
- stack1: [3, 4]stack2操作(条件二):
- nums[3] = 4 > nums[4] = 1 ✓,弹出位置3dp[4] = min(∞, dp[3] + costs[4]) = min(∞, 10 + 2) = 12
- nums[2] = 4 > nums[4] = 1 ✓,弹出位置2dp[4] = min(12, dp[2] + costs[4]) = min(12, 6 + 2) = 8
- nums[1] = 2 > nums[4] = 1 ✓,弹出位置1dp[4] = min(8, dp[1] + costs[4]) = min(8, 7 + 2) = 9
- 最终 dp[4] = 8
- stack2: [4]dp状态:[0, 7, 6, 10, 8]跳跃路径:0 → 2 → 4 (代价8)
最优路径
最终答案:dp[4] = 8最优路径:0 → 2 → 4
详细分析:
- 从位置0跳到位置2:满足条件一 (nums[0]=3 <= nums[2]=4,中间nums[1]=2 < nums[0]=3)
- 从位置2跳到位置4:满足条件二 (nums[2]=4 > nums[4]=1,中间nums[3]=4 >= nums[2]=4)
- 总代价:costs[2] + costs[4] = 6 + 2 = 8
详细代码实现
Java 实现
class Solution {public long minCost(int[] nums, int[] costs) {int n = nums.length;long[] dp = new long[n];// 初始化dp数组Arrays.fill(dp, Long.MAX_VALUE);dp[0] = 0; // 起始位置代价为0// 两个单调栈Deque<Integer> stack1 = new ArrayDeque<>(); // 处理条件一Deque<Integer> stack2 = new ArrayDeque<>(); // 处理条件二for (int j = 0; j < n; j++) {// 处理条件一:nums[i] <= nums[j] 且中间元素都小于 nums[i]// 单调递减栈,当遇到更大的元素时弹出while (!stack1.isEmpty() && nums[stack1.peek()] <= nums[j]) {int i = stack1.pop();dp[j] = Math.min(dp[j], dp[i] + costs[j]);}stack1.push(j);// 处理条件二:nums[i] > nums[j] 且中间元素都大于等于 nums[i]// 单调递增栈,当遇到更小的元素时弹出while (!stack2.isEmpty() && nums[stack2.peek()] > nums[j]) {int i = stack2.pop();dp[j] = Math.min(dp[j], dp[i] + costs[j]);}stack2.push(j);}return dp[n - 1];}
}
C# 实现
public class Solution {public long MinCost(int[] nums, int[] costs) {int n = nums.Length;long[] dp = new long[n];// 初始化dp数组for (int i = 0; i < n; i++) {dp[i] = long.MaxValue;}dp[0] = 0; // 起始位置代价为0// 两个单调栈Stack<int> stack1 = new Stack<int>(); // 处理条件一Stack<int> stack2 = new Stack<int>(); // 处理条件二for (int j = 0; j < n; j++) {// 处理条件一:nums[i] <= nums[j] 且中间元素都小于 nums[i]while (stack1.Count > 0 && nums[stack1.Peek()] <= nums[j]) {int i = stack1.Pop();dp[j] = Math.Min(dp[j], dp[i] + costs[j]);}stack1.Push(j);// 处理条件二:nums[i] > nums[j] 且中间元素都大于等于 nums[i]while (stack2.Count > 0 && nums[stack2.Peek()] > nums[j]) {int i = stack2.Pop();dp[j] = Math.Min(dp[j], dp[i] + costs[j]);}stack2.Push(j);}return dp[n - 1];}
}
时间复杂度和空间复杂度
- 时间复杂度:O(n) - 每个元素最多入栈和出栈一次
- 空间复杂度:O(n) - dp数组和两个栈的空间