3629. Minimum Jumps to Reach End via Prime Teleportation
題目 / Problem
中文: 給一個長度為 n 的整數陣列 nums。從索引 0 出發,目標是抵達索引 n - 1。在任何索引 i,你可以做下列其中一件事:
- 相鄰步:跳到
i + 1或i - 1(不可越界)。 - 質數傳送:若
nums[i]本身是質數p,可瞬間跳到任何索引j != i,只要nums[j] % p == 0(nums[j]能被p整除)。
求抵達 n - 1 所需的最少跳躍次數。
English: You are given an integer array nums of length n. Starting at index 0, reach index n - 1. From any index i you may either:
- Adjacent step — move to
i + 1ori - 1. - Prime teleportation — if
nums[i]is a primep, jump to any indexj != iwithnums[j] % p == 0.
Return the minimum number of jumps.
Constraints: 1 ≤ n ≤ 10^5, 1 ≤ nums[i] ≤ 10^6.
Example: nums = [1, 2, 4, 6] → answer 2. Walk 0 → 1 (one step), then teleport from index 1 (where nums[1] = 2 is prime) to index 3 because nums[3] = 6 is divisible by 2.
名詞解釋 / Glossary
- 質數 / prime number — 大於 1 且只被 1 與自己整除的整數,如 2, 3, 5, 7。An integer > 1 whose only divisors are 1 and itself.
- 質因數分解 / prime factorization — 把整數寫成幾個質數的乘積,例如 12 = 2 · 2 · 3。Writing an integer as a product of primes.
- 最小質因數篩(SPF sieve) / smallest-prime-factor sieve — 一個預先計算
spf[x]的表格,spf[x]是x的最小質因數。有了它,任何x ≤ M都能用O(log x)時間分解:反覆讓x /= spf[x]。A precomputed table wherespf[x]is the smallest prime that dividesx; lets us factor anyx ≤ MinO(log x)by repeatedly dividing byspf[x]. - BFS(廣度優先搜尋) / breadth-first search — 從起點出發,按「距離一層一層」擴展。第一次抵達某節點時,距離保證最小,因此最短路徑可在無權圖中以線性時間求出。Layered exploration that, on an unweighted graph, gives the shortest distance to each node the first time it is reached.
- bucket / 桶(這裡指
bucket[p]) — 一個列表,存放所有「nums[j]能被質數p整除」的索引j。當我們站在某個nums[i] = p的索引時,這個桶就是傳送的所有候選目標。bucket[p]is the list of indicesjwhosenums[j]is divisible by primep— exactly the teleport destinations available when we're at an index whose value isp. - 桶清空技巧 / clear-after-use trick — 每個桶在 BFS 中只會用一次:第一次有任何
i從這個桶展開時,所有桶內索引就都被加進佇列了;第二次再展開只會得到更長的距離,所以可以直接清空。把總工作量從可能的O(n²)降到O(n)。Once a bucket has been expanded, every index in it has been queued at the optimal distance; later visits would only produce longer paths, so we clear the bucket. This caps total work atO(n)instead ofO(n²). unordered_map/ vector / queue(C++ STL) —unordered_map<K,V>是雜湊表(平均 O(1) 查找);vector<T>是動態陣列;queue<T>是 FIFO 佇列,配合 BFS。Hash map, dynamic array, FIFO queue.
思路
如果只考慮相鄰步,這就是一條直線,答案永遠是 n - 1。質數傳送提供了「一步跨越任意距離」的捷徑,所以我們要把它當成圖的邊:從索引 i(其中 nums[i] = p 是質數)連到所有 nums[j] % p == 0 的 j。這樣每條邊權重都是 1,求最少跳躍次數就是無權圖的最短路徑——正是 BFS 的場合。
直接的做法會踩到複雜度地雷:對每個 i 都掃過全陣列找傳送目標,是 O(n²),當 n = 10^5 太慢。關鍵觀察:對同一個質數 p,所有 nums[j] % p == 0 的 j 構成同一個目標集合(與 i 無關)。因此先預先建桶 bucket[p],包含所有能被 p 整除的索引。為了快速建桶,再用 SPF 篩在 O(M log log M) 時間預先算好 spf 表(M = max(nums)),就能 O(log v) 對每個值取出它的相異質因數,把索引加進對應的桶。
最後是核心優化——桶用過就清空。BFS 第一次因為某個 i(nums[i] = p)展開 bucket[p] 時,桶內所有索引都拿到當前最佳距離;以後若再有別的 i'(同樣 nums[i'] = p)想用這個桶,只會給出更差或相等的距離,沒意義。把桶清掉之後,BFS 的每個節點和每條傳送邊都最多處理一次,總工作量降到 O(n + 桶總大小) = O(n)。
If we only had adjacent steps the answer would trivially be n - 1; teleportation is the only thing that lets us skip ahead, so it dominates the problem. Model it as a graph where every edge has weight 1 — adjacency edges between i and i ± 1, plus, for each index i with a prime value p = nums[i], an edge from i to every j whose nums[j] is divisible by p. Minimum jumps = shortest path in an unweighted graph = BFS.
The naive BFS where every prime index re-scans the whole array is O(n²). Two ideas fix that. First, precompute bucket[p] = the indices whose values are divisible by p; this set depends only on p, not on which index we're sitting at. We populate buckets cheaply with a smallest-prime-factor sieve up to max(nums), which lets us factor any value in O(log v). Second — the crucial trick — once we've expanded bucket[p] from one prime-valued index, every index inside is already enqueued at its best possible BFS distance via that prime. Any later index with the same prime value would only re-enqueue them at an equal-or-worse distance, so we clear the bucket. Each adjacency edge fires twice and each bucket entry fires once, making the BFS linear.
逐步走查 / Walkthrough
Input / 輸入:nums = [1, 2, 4, 6], n = 4, maxv = 6.
Sieve (SPF table) / 篩出最小質因數:
x |
2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|
spf |
2 | 3 | 2 | 5 | 2 |
Buckets / 建桶(對每個 nums[i] 抓相異質因數):
value nums[i] |
distinct primes | append index i to … |
|---|---|---|
nums[0]=1 |
(none) | nothing |
nums[1]=2 |
{2} | bucket[2] |
nums[2]=4 |
{2} | bucket[2] |
nums[3]=6 |
{2, 3} | bucket[2], bucket[3] |
Result: bucket[2] = [1, 2, 3], bucket[3] = [3].
BFS (start at 0, dist[0] = 0):
| step | pop i |
d |
adjacency adds | prime? nums[i] |
bucket expansion | queue after | dist |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | push 1 (d=1) | 1 — no |
— | [1] |
[0,1,_,_] |
| 2 | 1 | 1 | push 2 (d=2) | 2 — yes, p=2 |
bucket[2]=[1,2,3]: 1,2 already known; push 3 (d=2). Clear bucket[2]. |
[2, 3] |
[0,1,2,2] |
| 3 | 2 | 2 | (1, 3 known) | 4 — no |
— | [3] |
[0,1,2,2] |
| 4 | 3 | 2 | i == n-1, return 2 ✅ |
— | — | — | — |
Answer / 答案 = 2.
Solution — C
/* 演算法 / Algorithm:
* 1. 線性最小質因數篩 (SPF) 預先算好 spf[x],使每個值能在 O(log x) 內分解。
* Build a smallest-prime-factor sieve so any value factors in O(log x).
* 2. 對每個 nums[i] 取出相異質因數 p,把 i 放進 bucket[p]。
* For each value, push its index into bucket[p] for every distinct prime p.
* 3. 從索引 0 做 BFS:鄰居是 i±1;若 nums[i] 是質數 p,再加上 bucket[p]
* 內所有索引,然後清空該桶(同一個桶只展開一次,避免 O(n^2))。
* BFS from index 0 with adjacency edges and prime-bucket edges; each
* bucket fires only once (we clear it after use) so total work is O(n).
*/
int minJumps(int* nums, int numsSize) {
int n = numsSize; // n 是陣列長度 / array length
if (n == 1) return 0; // 起點即終點 / already at goal
int maxv = 1; // 紀錄最大值 / track max value
for (int i = 0; i < n; i++) {
if (nums[i] > maxv) maxv = nums[i]; // 比較並更新 / update if larger
}
/* ---- 最小質因數篩 / Smallest-prime-factor sieve ----
calloc 配置記憶體並全填 0 / calloc allocates and zeroes the buffer */
int* spf = (int*)calloc(maxv + 1, sizeof(int));
for (int i = 2; i <= maxv; i++) { // 從 2 起檢查 / scan from 2
if (spf[i] == 0) { // 0 表示 i 是質數 / i is prime
for (int j = i; j <= maxv; j += i) { // 標記 i 的所有倍數 / mark multiples of i
if (spf[j] == 0) spf[j] = i; // 只在尚未填寫時寫入,保留最小者 / keep the smallest prime
}
}
}
/* ---- 第一輪:算每個質數桶要多少格 ----
First pass: count how many entries each bucket will hold. */
int* bcount = (int*)calloc(maxv + 1, sizeof(int)); // 桶大小 / bucket sizes
for (int i = 0; i < n; i++) {
int v = nums[i]; // v 是可破壞的拷貝 / mutable copy
while (v > 1) { // 拆掉所有質因數 / peel off all prime factors
int p = spf[v]; // 取最小質因數 / smallest prime factor of v
bcount[p]++; // 此桶 +1 / bucket size += 1
while (v % p == 0) v /= p; // 把 v 中所有 p 除掉,避免重複加 / strip all copies of p
}
}
/* ---- 用前綴和算每個桶在扁平陣列中的起點 ----
Prefix-sum the counts to get each bucket's start offset in a flat array. */
int* boff = (int*)malloc((maxv + 2) * sizeof(int)); // boff[p] = 桶 p 起點 / start of bucket p
boff[0] = 0;
for (int i = 0; i <= maxv; i++) {
boff[i + 1] = boff[i] + bcount[i]; // 累加 / running sum
}
int total = boff[maxv + 1]; // 所有桶總條目數 / total entries across all buckets
int* bdata = (int*)malloc((total > 0 ? total : 1) * sizeof(int)); // 扁平資料區 / flat storage
int* bfill = (int*)calloc(maxv + 1, sizeof(int)); // 各桶寫入指標 / per-bucket write cursor
/* ---- 第二輪:把索引塞進桶 / Second pass: actually fill buckets. ---- */
for (int i = 0; i < n; i++) {
int v = nums[i];
while (v > 1) {
int p = spf[v];
bdata[boff[p] + bfill[p]++] = i; // 寫到桶尾並把指標往前推 / append and advance cursor
while (v % p == 0) v /= p;
}
}
/* ---- BFS 距離陣列與佇列 / BFS distance array and queue ---- */
int* dist = (int*)malloc(n * sizeof(int)); // dist[i] = 從 0 到 i 的最短跳數 / shortest jumps to i
for (int i = 0; i < n; i++) dist[i] = -1; // -1 代表還沒拜訪 / unvisited sentinel
int* queue = (int*)malloc(n * sizeof(int)); // 環狀不需要:節點最多入隊一次 / each node is pushed at most once
int qh = 0, qt = 0; // 頭尾指標 / head and tail cursors
queue[qt++] = 0; // 推入起點 / push start index
dist[0] = 0; // 起點距離為 0 / start has distance 0
int ans = -1; // 預設答案 / default answer
while (qh < qt) { // 佇列非空 / while queue is non-empty
int i = queue[qh++]; // 取出隊首 / pop the front
if (i == n - 1) { ans = dist[i]; break; } // 抵達終點即可離開 / early exit at goal
int d = dist[i]; // 當前距離 / current distance
/* 相鄰步 / Adjacent steps */
if (i + 1 < n && dist[i + 1] == -1) { // 右鄰未訪問 / right neighbor unvisited
dist[i + 1] = d + 1; // 距離 +1 / one more jump
queue[qt++] = i + 1; // 入隊 / enqueue
}
if (i - 1 >= 0 && dist[i - 1] == -1) { // 左鄰未訪問 / left neighbor unvisited
dist[i - 1] = d + 1;
queue[qt++] = i - 1;
}
/* 質數傳送 / Prime teleportation (only when nums[i] itself is prime) */
int v = nums[i];
if (v >= 2 && spf[v] == v) { // spf[v]==v 即 v 是質數 / v is prime
int sz = bcount[v]; // 桶內剩多少個 / remaining bucket size
int start = boff[v]; // 桶起始位置 / bucket start offset
for (int k = 0; k < sz; k++) {
int j = bdata[start + k]; // 桶內第 k 個索引 / k-th index in the bucket
if (dist[j] == -1) { // 尚未拜訪才更新 / only first visit wins (BFS optimal)
dist[j] = d + 1;
queue[qt++] = j;
}
}
bcount[v] = 0; // 清空桶:之後同樣的 p 不再重展 / clear bucket so each prime fires once
}
}
/* 釋放所有 malloc/calloc 配置的記憶體 / Free every heap allocation */
free(spf); free(bcount); free(boff);
free(bdata); free(bfill); free(dist); free(queue);
return ans;
}
Solution — C++
/* 演算法 / Algorithm:
* 1. 用 SPF 篩預算最小質因數 / SPF sieve up to max(nums).
* 2. 為每個出現的相異質因數 p 建桶 bucket[p] / build bucket[p] of indices divisible by p.
* 3. BFS:相鄰邊 + 若 nums[i] 是質數 p 則展開 bucket[p] 後清空 / BFS with prime-bucket edges, clearing buckets after use.
*/
class Solution {
public:
int minJumps(vector<int>& nums) {
int n = (int)nums.size(); // 陣列長度 / array length
if (n == 1) return 0; // 已在終點 / already at goal
// *max_element 回傳指向最大元素的迭代器,前面加 * 取值 / returns iterator; * dereferences it
int maxv = *max_element(nums.begin(), nums.end());
// vector<int>(size, init): 建立大小為 size、初值為 init 的動態陣列 / sized + initialized vector
vector<int> spf(maxv + 1, 0);
for (int i = 2; i <= maxv; ++i) {
if (spf[i] == 0) { // i 是質數 / i is prime
for (int j = i; j <= maxv; j += i) { // 走訪所有倍數 / walk multiples
if (spf[j] == 0) spf[j] = i; // 只在還沒寫過時寫,保留最小 / keep smallest factor
}
}
}
// unordered_map: 雜湊表,平均 O(1) 查找 / hash map with avg O(1) lookup
// vector<int>: 每個質數對應的索引列表 / per-prime list of indices
unordered_map<int, vector<int>> bucket;
for (int i = 0; i < n; ++i) {
int v = nums[i];
while (v > 1) { // 持續拆出質因數 / peel prime factors
int p = spf[v];
bucket[p].push_back(i); // 加進桶尾 / append index to bucket
while (v % p == 0) v /= p; // 把所有 p 除掉,避免同一個 p 加兩次 / strip all copies of p
}
}
vector<int> dist(n, -1); // -1 表示未訪 / -1 = unvisited
queue<int> q; // STL FIFO 佇列 / FIFO queue
q.push(0);
dist[0] = 0;
while (!q.empty()) {
int i = q.front(); // 看隊首 / peek front
q.pop(); // 真正彈出 / pop it (q.pop returns void)
if (i == n - 1) return dist[i]; // 到終點即返回 / early exit
int d = dist[i];
// 範圍 for + initializer_list 一次處理兩個鄰居 / range-for over a brace-list iterates {i-1, i+1}
for (int ni : {i - 1, i + 1}) {
if (ni >= 0 && ni < n && dist[ni] == -1) { // 邊界內且未訪 / in-bounds and unvisited
dist[ni] = d + 1;
q.push(ni);
}
}
int v = nums[i];
if (v >= 2 && spf[v] == v) { // v 是質數 / v is prime
auto it = bucket.find(v); // it 是迭代器 / iterator into the map
if (it != bucket.end()) { // 桶存在 / bucket exists
for (int j : it->second) { // it->second 是 vector<int> / second = the index list
if (dist[j] == -1) {
dist[j] = d + 1;
q.push(j);
}
}
it->second.clear(); // 清空桶以避免重展 / clear so each prime fires once
}
}
}
return dist[n - 1]; // 理論上必能到達 / always reachable in practice
}
};
複雜度 / Complexity
設 n = nums.length,M = max(nums) ≤ 10^6。Let n be the array length and M the maximum value.
- Time:
O(M log log M + n · k),其中k ≤ 7是任一個值的相異質因數上限(因為2·3·5·7·11·13·17 = 510510,再乘下一個質數就超過10^6)。 - SPF 篩本身是
O(M log log M)。BFS 中每個索引最多入隊一次,每條傳送邊只觸發一次(因為桶用後即清),所以 BFS 總工作量是O(n + 桶總條目) = O(n · k)。 - The sieve costs
O(M log log M). Each index is enqueued at most once and each bucket entry expanded at most once thanks to the clear-after-use trick, so BFS is linear innplus the total number of bucket entries (≤ 7n). - Space:
O(M + n)。 spf、bcount、boff都是O(M);BFS 的dist/queue是O(n);扁平桶資料bdata至多7n個整數。O(M)for the sieve and per-prime arrays;O(n)for the BFS queue, distances, and the flat bucket data (bounded by7n).
Pitfalls & Edge Cases
- 不要對非質數
nums[i]做傳送 / Don't teleport from non-prime values. 題目明確規定只有當nums[i]本身是質數時才能傳送。即使nums[i] = 6有質因數 2、3,從這個位置也不能用。代碼用spf[v] == v來確認v是質數。 nums[i] = 1沒有質因數 / Value1contributes no prime factors. 跳過分解迴圈即可,不要把 1 當質數。while (v > 1)自然處理掉這種情況。- 桶用完一定要清空 / Always clear a bucket after expansion. 沒清空的話,第二次抵達同一個質數值的索引時會重新掃整個桶,最壞退化到
O(n²),在n = 10^5上會超時。 - BFS 一律用「第一次設距離才入隊」 / Only enqueue on the first time we set a distance. 後續任何路徑都不會更短,重複入隊只會白白吃時間並導致同一節點被處理多次。
- 記得檢查
i = n - 1早退 / Check goal early to skip extra work. 不檢查也正確,但抵達後立即停可省下不必要的展開。 - C 版的記憶體配置順序 / Watch the C allocation order. 必須先用第一輪
bcount算前綴和boff,才能配置扁平bdata並在第二輪寫入;兩輪分開可避免動態擴容。 - 質數傳送的「
j != i」/j != iconstraint is automatically respected. 從i展開時dist[i]已是 0 或某個正值(不會是 -1),所以同一個i不會把自己再加進佇列。 - 空間:
maxv最大10^6,spf佔約 4 MB /spfforM = 10^6is ~4 MB. 在 LeetCode 限制內,但若再多陣列要小心。