LeetCode 每日一题笔记
0. 前言
- 日期:2026.05.08
- 题目:3629. 素数跳跃最小次数
- 难度:中等
- 标签:数组 广度优先搜索 哈希表 数学(素数)
1. 题目理解
问题描述 :
给你一个整数数组 nums,你可以从下标 i 跳跃到下标 j,满足以下任一条件:
j = i + 1或j = i - 1(相邻下标);nums[i]和nums[j]存在共同的素数因子。
请你返回从数组起始下标 0 到末尾下标 n-1 的最小跳跃次数 ,若无法到达则返回 -1。
示例:
输入:nums = [2,3,4,9]
输出:2
解释:
最优路径:0 → 2 → 3,共2次跳跃。
- 0到2:nums[0]=2 和 nums[2]=4 共享素因子2;
- 2到3:nums[2]=4 和 nums[3]=9 无共享素因子,走相邻下标跳跃。
2. 解题思路
核心观察
- 最小跳跃次数属于无权图最短路径问题,BFS是天然的最优解法;
- 两种跳跃规则本质是图的边,相邻下标为基础边,共享素因子为批量连通边;
- 需先预处理素数判断与质因数分解,建立素数到对应下标的映射,避免重复计算;
- 处理过的素数可直接移除,避免重复入队,大幅优化BFS效率。
算法步骤
- 素数预处理:遍历数组,判断每个元素是否为素数,存入素数集合;
- 质因数分解:对数组每个元素分解素因子,建立「素数→对应下标列表」的哈希映射;
- BFS求最短路径 :
- 初始化队列,从下标0开始,标记已访问节点;
- 每层遍历对应一步跳跃,先处理相邻下标的合法跳跃,再处理同素因子的批量跳跃;
- 处理完的素数从映射中移除,避免重复处理;
- 到达数组末尾下标时,立即返回当前步数;
- 若队列遍历完成仍未到达终点,返回-1。
3. 代码实现
java
package lc3629;
import java.util.*;
class Solution {
public static boolean isPrime(int num) {
if (num < 2) {
return false;
}
if (num == 2) {
return true;
}
if (num % 2 == 0) {
return false;
}
for (int i = 3; i <= Math.sqrt(num); i += 2) {
if (num % i == 0) {
return false;
}
}
return true;
}
public static Set<Integer> getPrimeFactors(int num) {
Set<Integer> factors = new HashSet<>();
if (num <= 1) return factors;
if (num % 2 == 0) {
factors.add(2);
while (num % 2 == 0) num /= 2;
}
for (int i = 3; i <= Math.sqrt(num); i += 2) {
if (num % i == 0) {
factors.add(i);
while (num % i == 0) num /= i;
}
}
if (num > 1) factors.add(num);
return factors;
}
public int bfs(int[] nums, HashMap<Integer, List<Integer>> map, Set<Integer> primeSet) {
int n = nums.length;
Queue<Integer> queue = new ArrayDeque<>(); // 优化3
boolean[] visited = new boolean[n];
queue.add(0);
visited[0] = true;
int step = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int curIdx = queue.poll();
if (curIdx == n - 1) return step;
if (curIdx - 1 >= 0 && !visited[curIdx - 1]) {
visited[curIdx - 1] = true;
queue.add(curIdx - 1);
}
if (curIdx + 1 < n && !visited[curIdx + 1]) {
visited[curIdx + 1] = true;
queue.add(curIdx + 1);
}
int curNum = nums[curIdx];
if (primeSet.contains(curNum)) {
List<Integer> targetList = map.get(curNum);
if (targetList != null) {
for (int targetIdx : targetList) {
if (!visited[targetIdx] && targetIdx != curIdx) {
visited[targetIdx] = true;
queue.add(targetIdx);
}
}
map.remove(curNum);
}
}
}
step++;
}
return -1;
}
public int minJumps(int[] nums) {
int n = nums.length;
if (n == 1) return 0;
Set<Integer> primeSet = new HashSet<>();
for (int num : nums) {
if (isPrime(num)) {
primeSet.add(num);
}
}
HashMap<Integer, List<Integer>> map = new HashMap<>();
for (int i = 0; i < n; i++) {
Set<Integer> factors = getPrimeFactors(nums[i]);
for (int p : factors) {
if (primeSet.contains(p)) {
map.computeIfAbsent(p, k -> new ArrayList<>()).add(i);
}
}
}
return bfs(nums, map, primeSet);
}
}
4. 代码优化说明
- 素数判断优化:提前排除偶数,仅遍历到数值平方根,减少无效循环次数;
- 质因数分解优化:先处理2的因子,再仅遍历奇数因子,降低循环次数;
- BFS队列优化:使用ArrayDeque替代LinkedList,提升入队、出队的执行效率;
- 去重优化:处理完一个素数的所有对应下标后,直接从哈希表移除该素数,避免重复入队和重复处理;
- 提前终止:BFS遍历中到达数组末尾时立即返回步数,无需遍历全图。
5. 复杂度分析
-
时间复杂度 :O(n×max(nums))O(n \times \sqrt{max(nums)})O(n×max(nums) )
- 素数判断与质因数分解的瓶颈为每个数值的平方根遍历,总复杂度为 O(n×max(nums))O(n \times \sqrt{max(nums)})O(n×max(nums) );
- BFS遍历中每个下标最多入队一次,每个素数最多处理一次,额外开销为线性级 O(n)O(n)O(n)。
-
空间复杂度 :O(n+p)O(n + p)O(n+p)
- nnn 为数组长度,用于存储访问数组、队列、哈希映射;
- ppp 为数组元素范围内的素数个数,用于存储素数集合与素数映射。
6. 总结
- 本题核心是BFS求无权图最短路径 + 素数分解建图,将跳跃规则转化为图的连通边,用BFS天然解决最小步数问题;
- 关键技巧:通过质因数分解建立素数到下标的映射,将共享素因子的跳跃转化为批量边处理,大幅减少建图开销;
- 优化核心在于素数相关计算的效率提升,以及BFS过程中的去重处理,避免无效遍历;
- 本题是图论搜索与数学素数知识结合的经典题型,BFS是解决此类最小步数问题的最优方案。