← 題庫 / Archive
2026-05-08 Daily Medium ArrayHash TableMathBreadth-First SearchNumber Theory

3629. Minimum Jumps to Reach End via Prime Teleportation

題目 / Problem

中文: 給一個長度為 n 的整數陣列 nums。從索引 0 出發,目標是抵達索引 n - 1。在任何索引 i,你可以做下列其中一件事:

  1. 相鄰步:跳到 i + 1i - 1(不可越界)。
  2. 質數傳送:若 nums[i] 本身是質數 p,可瞬間跳到任何索引 j != i,只要 nums[j] % p == 0nums[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:

  1. Adjacent step — move to i + 1 or i - 1.
  2. Prime teleportation — if nums[i] is a prime p, jump to any index j != i with nums[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 where spf[x] is the smallest prime that divides x; lets us factor any x ≤ M in O(log x) by repeatedly dividing by spf[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 indices j whose nums[j] is divisible by prime p — exactly the teleport destinations available when we're at an index whose value is p.
  • 桶清空技巧 / 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 at O(n) instead of O(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 == 0j。這樣每條邊權重都是 1,求最少跳躍次數就是無權圖的最短路徑——正是 BFS 的場合。

直接的做法會踩到複雜度地雷:對每個 i 都掃過全陣列找傳送目標,是 O(n²),當 n = 10^5 太慢。關鍵觀察:對同一個質數 p,所有 nums[j] % p == 0j 構成同一個目標集合(與 i 無關)。因此先預先建桶 bucket[p],包含所有能被 p 整除的索引。為了快速建桶,再用 SPF 篩在 O(M log log M) 時間預先算好 spf 表(M = max(nums)),就能 O(log v) 對每個值取出它的相異質因數,把索引加進對應的桶。

最後是核心優化——桶用過就清空。BFS 第一次因為某個 inums[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.lengthM = 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 in n plus the total number of bucket entries (≤ 7n).
  • Space: O(M + n)
  • spfbcountboff 都是 O(M);BFS 的 dist/queueO(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 by 7n).

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 沒有質因數 / Value 1 contributes 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 != i constraint is automatically respected.i 展開時 dist[i] 已是 0 或某個正值(不會是 -1),所以同一個 i 不會把自己再加進佇列。
  • 空間:maxv 最大 10^6spf 佔約 4 MB / spf for M = 10^6 is ~4 MB. 在 LeetCode 限制內,但若再多陣列要小心。