1 总结
1.1 贪心、动态规划和BFS/DFS题解的关系
一般能使用贪心、动态规划解决一个问题时,使用BFS,DFS也能解决这个题,但是反之不能成立。
1.2
2 贪心 -> BFS/DFS
2.1 跳跃游戏1和3的异同
这两道题,“跳跃游戏”(题号 55)和“跳跃游戏 III”(题号 1306),虽然都是关于跳跃问题的,但它们的规则和解题策略有显著的不同。下面是它们的主要异同点:
2.1.1 跳跃游戏(题号 55)
-
问题描述:给定一个非负整数数组
nums
,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。需要判断是否能够到达最后一个下标。 -
跳跃规则:从任意位置
i
,可以向前跳跃0
到nums[i]
的任意步数。 -
目标:到达数组的最后一个下标。
-
解题策略:通常使用贪心算法。从左到右遍历数组,更新能够到达的最远位置。如果最远位置超过或等于最后一个下标,则返回
true
。
2.1.2 跳跃游戏 III(题号 1306)
-
问题描述:这里有一个非负整数数组
arr
,你最开始位于该数组的起始下标start
处。当你位于下标i
处时,你可以跳到i + arr[i]
或者i - arr[i]
。 -
跳跃规则:从任意位置
i
,只能向前跳到i + arr[i]
或向后跳到i - arr[i]
。 -
目标:跳到任何一个元素值为
0
的下标处。 -
解题策略:通常使用深度优先搜索(DFS)或广度优先搜索(BFS)。由于跳跃方向可以正向也可以反向,需要记录访问过的位置以防止无限循环。
2.1.3 它们的异同点
-
跳跃方向和范围:
- 在“跳跃游戏”中,跳跃只能向前,且范围在
0
到nums[i]
之间。 - 在“跳跃游戏 III”中,跳跃可以向前或向后,且步数固定为
arr[i]
。
- 在“跳跃游戏”中,跳跃只能向前,且范围在
-
目标不同:
- “跳跃游戏”的目标是到达数组的最后一个下标。
- “跳跃游戏 III”的目标是到达任一元素值为
0
的下标。
-
解题策略:
- “跳跃游戏”通常用贪心算法解决。
- “跳跃游戏 III”则更适合使用DFS或BFS,因为它涉及到多个可能的跳跃方向和回溯。
这两个问题虽然在表面上看起来类似,但实际上涉及到的算法思维和解决方法有很大的区别。
2.2 LC55. 跳跃游戏(贪心)
public boolean canJump(int[] nums) {int n=nums.length;int e=0;boolean res=true;for(int i=0;i<n;i++){if(i>e){res=false;break;}e=Math.max(e, i+nums[i]);}return res;}
2.3 LC1306. 跳跃游戏 III
class Solution {public boolean canReach(int[] arr, int start) {int n=arr.length;boolean[]vis=new boolean[n];return dfs(arr,start,vis);}boolean dfs(int[] arr, int start,boolean[]vis){vis[start]=true;if(arr[start]==0){return true;}int rm=arr[start]+start;int lm=start-arr[start];boolean l=false;boolean r=false;if(rm<arr.length&&!vis[rm])l=dfs(arr,arr[start]+start,vis);if(lm>=0&&!vis[lm])r=dfs(arr,start-arr[start],vis);return l||r;}// bfs方法解题public boolean canReach2(int[] arr, int start) {int n=arr.length;Deque<Integer>st=new LinkedList<>();st.addLast(start);boolean[]vis=new boolean[n];while(!st.isEmpty()){int poll=st.pollLast();vis[poll]=true;if(arr[poll]==0){return true;}if(poll+arr[poll]<n&&!vis[poll+arr[poll]])st.addLast(poll+arr[poll]);if(poll-arr[poll]>=0&&!vis[poll-arr[poll]])st.addLast(poll-arr[poll]);}return false;}
}
2.4 LC1345. 跳跃游戏 IV
2.4.1 答案:为什么minJumps2超时了?
class Solution {public int minJumps(int[] arr) {Map<Integer, List<Integer>> idxSameValue = new HashMap<Integer, List<Integer>>();for (int i = 0; i < arr.length; i++) {idxSameValue.putIfAbsent(arr[i], new ArrayList<Integer>());idxSameValue.get(arr[i]).add(i);}Set<Integer> visitedIndex = new HashSet<Integer>();Queue<int[]> queue = new ArrayDeque<int[]>();queue.offer(new int[]{0, 0});visitedIndex.add(0);while (!queue.isEmpty()) {int[] idxStep = queue.poll();int idx = idxStep[0], step = idxStep[1];if (idx == arr.length - 1) {return step;}int v = arr[idx];step++;if (idxSameValue.containsKey(v)) {for (int i : idxSameValue.get(v)) {if (visitedIndex.add(i)) {queue.offer(new int[]{i, step});}}idxSameValue.remove(v);}if (idx + 1 < arr.length && visitedIndex.add(idx + 1)) {queue.offer(new int[]{idx + 1, step});}if (idx - 1 >= 0 && visitedIndex.add(idx - 1)) {queue.offer(new int[]{idx - 1, step});}}return -1;}public int minJumps2(int[] arr) {int n=arr.length;Map<Integer,List<Integer>>mp=new HashMap<>();for(int i=0;i<n;i++){List<Integer>ls=mp.getOrDefault(arr[i],new ArrayList<>());ls.add(i);mp.put(arr[i],ls);}Deque<Integer>q=new LinkedList<>();q.offerLast(0);int ans=0;boolean[]vis=new boolean[n];while(!q.isEmpty()){int size=q.size();for(int i=0;i<size;i++){int poll=q.pollFirst();if(poll==n-1){return ans;}if(poll+1<n&&!vis[poll+1]){vis[poll+1]=true;q.offerLast(poll+1);}if(poll-1>=0&&!vis[poll-1]){vis[poll-1]=true;q.offerLast(poll-1);}List<Integer>t=mp.get(arr[poll]);for(int j=0;j<t.size();j++){if(!vis[t.get(j)]&&t.get(j)!=poll){vis[t.get(j)]=true;q.offerLast(t.get(j));}}}ans++;}return -1;}
}
让我们分析这两个方法 minJumps
和 minJumps2
的不同之处,以了解为什么 minJumps2
会超时。
2.4.2 方法 minJumps
- 数据结构:使用
HashMap
和HashSet
来存储相同值的索引和已访问的索引。使用Queue
来进行广度优先搜索(BFS)。 - 访问控制:通过
visitedIndex
(一个HashSet
)控制访问,一旦访问过某个索引,就不会再访问。 - 优化处理:在访问完所有相同值的索引后,使用
idxSameValue.remove(v)
来移除这个值,从而减少后续的无效搜索。
2.4.3 方法 minJumps2
- 数据结构:使用类似的
HashMap
来存储相同值的索引。使用Deque
(双端队列)进行BFS。 - 访问控制:通过一个
boolean
数组vis
控制访问,记录已访问的索引。 - 重复检查:方法中没有移除已经访问过的值的索引集合,这意味着即使某个值的所有索引都已经被访问,它们仍然会在后续的搜索中被重复检查。
2.4.4 为什么 minJumps2
超时
原因可能在于 minJumps2
没有像 minJumps
那样有效地减少搜索空间。在 minJumps
中,一旦一个值的所有可能的跳跃位置都被探索过,这个值就会从 idxSameValue
中移除,从而避免了对这些位置的重复搜索。而 minJumps2
没有这样的优化,因此在遇到具有大量相同值的数组时,它会进行许多不必要的重复检查,这可能导致超时。
另外,虽然 minJumps
和 minJumps2
都是BFS实现,但由于对搜索空间的处理方式不同,两者在面对大规模数据时的性能表现会有显著差异。在这种情况下,减少无效的重复搜索是优化算法和避免超时的关键。
2.5 方法二改进
public int minJumps(int[] arr) {int n=arr.length;Map<Integer,List<Integer>>mp=new HashMap<>();for(int i=0;i<n;i++){List<Integer>ls=mp.getOrDefault(arr[i],new ArrayList<>());ls.add(i);mp.put(arr[i],ls);}Deque<Integer>q=new LinkedList<>();q.offerLast(0);int ans=0;boolean[]vis=new boolean[n];while(!q.isEmpty()){int size=q.size();for(int i=0;i<size;i++){int poll=q.pollFirst();if(poll==n-1){return ans;}if(poll+1<n&&!vis[poll+1]){vis[poll+1]=true;q.offerLast(poll+1);}if(poll-1>=0&&!vis[poll-1]){vis[poll-1]=true;q.offerLast(poll-1);}if(mp.containsKey(arr[poll])){List<Integer>t=mp.get(arr[poll]);for(int j=0;j<t.size();j++){if(!vis[t.get(j)]&&t.get(j)!=poll){vis[t.get(j)]=true;q.offerLast(t.get(j));}}mp.remove(arr[poll]);}}ans++;}return -1;}