2770. Maximum Number of Jumps to Reach the Last Index
題目 / Problem
中文:
給定一個 0-indexed 的整數陣列 nums(長度為 n)和一個整數 target。一開始你站在索引 0。每一步你可以從索引 i 跳到索引 j,條件是:
- 0 <= i < j < n
- -target <= nums[j] - nums[i] <= target
請回傳你能跳到索引 n - 1 的最大跳躍次數。如果無法到達最後一個位置,回傳 -1。
English:
Given a 0-indexed integer array nums of length n and an integer target. You start at index 0. In one step you can jump from i to j if:
- 0 <= i < j < n
- -target <= nums[j] - nums[i] <= target
Return the maximum number of jumps to reach index n - 1, or -1 if it is impossible.
Constraints:
- 2 <= n <= 1000
- -10^9 <= nums[i] <= 10^9
- 0 <= target <= 2 * 10^9
Worked example: nums = [1,3,6,4,1,2], target = 2 → 3. One optimal path: 0 → 1 → 3 → 5.
名詞解釋 / Glossary
- 動態規劃 / Dynamic Programming (DP) — 把大問題拆成重疊的小子問題並把答案存起來,避免重複計算。Solving a problem by breaking it into overlapping subproblems and reusing each subproblem's answer.
- 狀態 / State — DP 中代表「子問題」的變數組合;本題的狀態是「目前停在哪個索引」。The variables describing one subproblem; here it's "which index am I currently standing on".
- 狀態轉移 / Transition — 從一個狀態推到下一個狀態的規則;這裡是
dp[j] = max(dp[j], dp[i] + 1)。The rule that derives one state from a previous one. - 不可達哨兵值 / Unreachable Sentinel — 用一個特殊值(例如
-1)表示「這個位置從 0 跳不到」。A special value (e.g.-1) marking a state that can never be entered. malloc/free(C) —malloc(n * sizeof(T))在堆積 (heap) 上配置一塊記憶體並回傳指標;用完要free釋放。Allocates and releases heap memory in C.std::vector(C++) — 自動管理長度與記憶體的動態陣列,相當於 C 的「malloc + free」打包好。A self-managing dynamic array; no manualfreeneeded.std::max— 回傳兩個值中較大的那個。Returns the larger of two values.
思路
最直覺的暴力做法是嘗試所有跳躍順序:從 0 出發遞迴地往後跳,每跳一次就 +1,最後取最大值。但每個位置都可能跳到後面任何一個位置,狀態空間呈指數爆炸,n = 1000 必定逾時。觀察發現「從 0 走到索引 j 的最大跳躍次數」只跟「我在哪一格」有關,跟「我用什麼路線走過來」無關 — 這正是動態規劃的訊號。我們定義 dp[j] = 從索引 0 到 j 的最大跳躍次數,初始化 dp[0] = 0,其他位置設成 -1 代表不可達。對每個 j 從 1 掃到 n-1,再對每個 i < j 檢查 (a) dp[i] 是否可達;(b) |nums[j] - nums[i]| <= target 是否成立。若兩條件都成立,就用 dp[i] + 1 去更新 dp[j]。為什麼可以這樣安全地由小推大?因為 j 只會從 i < j 的位置轉移過來,外層按 j 遞增順序計算時,所有需要的 dp[i] 都已經確定了。最後 dp[n-1] 就是答案;若仍是 -1 表示不可達。
The brute-force idea is to try every jumping sequence recursively from index 0, but the number of paths is exponential and 1000 elements would never finish in time. The key observation is that "the maximum number of jumps to reach index j" depends only on which index we're on — not on the path we took to get there — which is exactly when DP applies. Define dp[j] as the maximum number of jumps from 0 to j, with dp[0] = 0 and all others initialized to -1 (the "unreachable" sentinel). Iterate j from 1 to n-1 and, for each j, scan every earlier i < j: if dp[i] is reachable and |nums[j] - nums[i]| <= target, relax dp[j] = max(dp[j], dp[i] + 1). The order is safe because dp[j] only ever reads from strictly smaller indices, all of which were finalized in previous outer iterations. The answer is dp[n-1] (or -1 if it stayed unreachable). With n <= 1000, the O(n^2) work is comfortably fast.
逐步走查 / Walkthrough
Input: nums = [1, 3, 6, 4, 1, 2], target = 2. Initialize dp = [0, -1, -1, -1, -1, -1].
| j | nums[j] | 檢查的 i / valid i | 條件 / check | dp 更新後 / dp after |
|---|---|---|---|---|
| 1 | 3 | i=0: |3-1|=2 ✓, dp[0]+1=1 | dp[1] ← 1 | [0, 1, -1, -1, -1, -1] |
| 2 | 6 | i=0: |6-1|=5 ✗; i=1: |6-3|=3 ✗ | none | [0, 1, -1, -1, -1, -1] |
| 3 | 4 | i=0: |4-1|=3 ✗; i=1: |4-3|=1 ✓, dp[1]+1=2; i=2: dp[2]=-1 skip | dp[3] ← 2 | [0, 1, -1, 2, -1, -1] |
| 4 | 1 | i=0: |1-1|=0 ✓, dp[0]+1=1; i=1: |1-3|=2 ✓, dp[1]+1=2; i=2: skip; i=3: |1-4|=3 ✗ | dp[4] ← 2 | [0, 1, -1, 2, 2, -1] |
| 5 | 2 | i=0: |2-1|=1 ✓, gives 1; i=1: |2-3|=1 ✓, gives 2; i=2: skip; i=3: |2-4|=2 ✓, gives 3; i=4: |2-1|=1 ✓, gives 3 | dp[5] ← 3 | [0, 1, -1, 2, 2, 3] |
Final answer = dp[5] = 3. ✅
Solution — C
// 演算法:DP,dp[j] = 從索引 0 跳到 j 的最大次數,-1 表不可達
// Algorithm: DP where dp[j] = max jumps from 0 to j, -1 means unreachable.
// 對每個 j,掃所有 i<j,若 |nums[j]-nums[i]|<=target 則用 dp[i]+1 鬆弛 dp[j]
// For each j, scan every i<j and relax dp[j] = max(dp[j], dp[i]+1) when in range.
// 時間 O(n^2),空間 O(n)。Time O(n^2), space O(n).
#include <stdlib.h> // malloc / free 的標頭 / header for malloc and free
int maximumJumps(int* nums, int numsSize, int target) {
// 在堆積上配置長度為 n 的 int 陣列 / allocate an int array of length n on the heap
int* dp = (int*)malloc(sizeof(int) * numsSize);
// 把全部位置先設為 -1,代表「目前還無法從 0 跳到這裡」
// Initialize every entry to -1, meaning "not yet known to be reachable from 0".
for (int i = 0; i < numsSize; i++) {
dp[i] = -1;
}
dp[0] = 0; // 起點本身不需要跳,所以是 0 / starting point needs zero jumps
// 外層 j:依序計算每個目標索引;因為只往回看 i<j,順序保證子問題已算好
// Outer loop j: compute each target index in order; since we only look back at i<j,
// every dp[i] we read has already been finalized.
for (int j = 1; j < numsSize; j++) {
// 內層 i:列舉所有可能跳到 j 的前一格 / inner loop: every candidate previous index
for (int i = 0; i < j; i++) {
// 若 i 本身就跳不到,當然不能從 i 再跳到 j / skip unreachable predecessors
if (dp[i] == -1) continue;
// 用 long 避免兩個 ±1e9 相減在 int 邊界附近的疑慮 (純為教學保險)
// Use long to dodge any int-edge worries when subtracting two ±1e9 values.
long diff = (long)nums[j] - (long)nums[i];
// 條件:差值落在 [-target, target] 區間 / jump is legal iff diff is in range
if (diff >= -(long)target && diff <= (long)target) {
// 鬆弛:dp[j] 取「自己」與「從 i 過來再 +1」中較大者
// Relax: keep the larger of the current dp[j] and dp[i]+1.
if (dp[i] + 1 > dp[j]) {
dp[j] = dp[i] + 1;
}
}
}
}
int ans = dp[numsSize - 1]; // 終點的答案(可能仍是 -1)/ answer at the last index
free(dp); // 釋放剛剛 malloc 的記憶體 / release heap memory
return ans;
}
Solution — C++
// 演算法與 C 版相同:dp[j] 為從 0 到 j 的最大跳躍次數,-1 表不可達
// Same algorithm as the C version: dp[j] = max jumps from 0 to j, -1 = unreachable.
// 用 std::vector 與 std::max 寫得更簡潔 / written with std::vector and std::max for clarity.
// 時間 O(n^2),空間 O(n)。Time O(n^2), space O(n).
#include <vector> // std::vector 動態陣列 / dynamic array container
#include <algorithm> // std::max
using namespace std;
class Solution {
public:
int maximumJumps(vector<int>& nums, int target) {
int n = nums.size(); // n 是陣列長度,型別 int 已足夠 / array length, int suffices
// vector<int> dp(n, -1):建立長度 n 的向量,每格初始化為 -1(不可達哨兵)
// vector<int> dp(n, -1) creates a length-n vector, every slot set to -1 (sentinel).
vector<int> dp(n, -1);
dp[0] = 0; // 起點不用跳 / starting index needs no jumps
// 由小到大計算每個目標索引 j / fill dp[j] in increasing order of j
for (int j = 1; j < n; ++j) {
for (int i = 0; i < j; ++i) {
if (dp[i] == -1) continue; // i 不可達就跳過 / skip unreachable i
// 轉成 long long,徹底避免兩個 1e9 量級整數相減/比較的溢位疑慮
// Cast to long long to be completely safe against int overflow concerns.
long long diff = (long long)nums[j] - (long long)nums[i];
if (diff >= -(long long)target && diff <= (long long)target) {
// std::max 回傳兩者較大值,等同於手寫的 if 判斷
// std::max returns the larger of the two arguments.
dp[j] = max(dp[j], dp[i] + 1);
}
}
}
return dp[n - 1]; // 若仍為 -1 表示無法到達 / -1 means terminal index unreachable
}
};
複雜度 / Complexity
- Time:
O(n^2)— 外層j走n次、內層i最多走j次,總共約n*(n-1)/2次比較。n是nums的長度。The outer loop runsntimes and the inner loop up tojtimes, giving roughlyn^2 / 2constant-work checks. Heren = nums.length. - Space:
O(n)— 只有一個長度為n的dp陣列;其他都是常數變數。Only thedparray of lengthn; everything else is a handful of scalar variables.
Pitfalls & Edge Cases
- 回傳 -1,而不是 0 — 不可到達的終點要回
-1,但起點本身需要 0 步,兩者都用dp但意義不同。Don't confuse "unreachable" (-1) with "starting point" (0); both live indp, but mean different things. - 不可達不能拿來轉移 / Don't relax from unreachable predecessors — 如果忘記
if (dp[i] == -1) continue;,會把-1 + 1 = 0誤當成「跳一次就到」,答案會錯。Without the skip,-1 + 1 = 0looks like a valid one-jump path and corrupts later states. - 絕對值寫法 / Range check — 條件是
-target <= diff <= target,請勿誤寫成abs(diff) <= target後又忘了target可能很大 — 雖然數學等價,但拆成兩個比較對初學者更清楚。Mathematically equivalent toabs(diff) <= target, but writing the two comparisons makes the intent clearer. - 整數溢位 / Integer overflow —
nums[i]可達±10^9,差值可到2 * 10^9;target上限也是2 * 10^9。雖然剛好塞得進 32-bitint,但寫成long/long long比較最保險,也方便讀者理解我們在意這件事。Edge of the int32 range — usinglong/long longfor the diff avoids subtle bugs and signals the concern. target == 0— 只有「值完全相等」的兩格才能互跳;常常導致無法到達,要正確回傳-1(見 Example 3)。Only equal-valued indices can jump to each other; often produces the-1case.- 記得
free/ Memory leak in C — C 版用malloc後要free,否則 LeetCode 雖不會判錯,但是好習慣。Alwaysfreewhat youmalloc— leaks won't fail LeetCode but are bad practice.