1. 跳跃游戏
(1)贪心
不用拘泥于每次究竟跳跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。
class Solution {
public boolean canJump(int[] nums) {
if (nums.length == 1) {
return true;
}
//覆盖范围
int coverRange = nums[0];
//在覆盖范围内更新最大的覆盖范围
for (int i = 0; i <= coverRange; i++) {
coverRange = Math.max(coverRange, i + nums[i]);
if (coverRange >= nums.length - 1) {
return true;
}
}
return false;
}
}
(2)模拟
class Solution {
public boolean canJump(int[] nums) {
int max = 0;
int len = nums.length;
//max相当于现在可以到达的最远位置
//遍历数组 从索引为0的地方开始
for (int i = 0; i < len; i++) {
//最远都不能到达索引为i处(i是 ++的)无法继续前进 直接返回false
if(i > max)
return false;
//实时更新可以到达的最远位置
max = Math.max(max,i+nums[i]);
}
return true;
}
}
2. 跳跃游戏2
(1)DFS(不推荐)
DFS比较符合正常人的解题思路,以cur为起点,进行递归。
nums[cur]获取最大跳数
遍历1~nums[cur]次,计算next的位置
剪枝条件:如果minSteps[next] <= minSteps[cur] + 1; 已有方案更好,不需要递归了
终止条件:可选
if (minSteps[cur] > minSteps[nums.length - 1]) {
return;
}
代码:
public class Solution {
int[] minSteps;
public int jump(int[] nums) {
//排除特例
if (nums == null || nums.length <= 1)
return 0;
//存储到当前位置,所需要的最小步数
minSteps = new int[nums.length];
Arrays.fill(minSteps, Integer.MAX_VALUE);
minSteps[0] = 0;
dfs(nums, 0);
return minSteps[nums.length - 1];
}
private void dfs(int[] nums, int cur) {
for (int next = cur + 1; next < nums.length && next <= cur + nums[cur]; next++) {
// 剪枝。当前的方案比已有的方案还差,就不要递归了
if (minSteps[next] <= minSteps[cur] + 1)
continue;
minSteps[next] = minSteps[cur] + 1;
dfs(nums, next); // 递归尝试所有跳数
}
}
}
(2)BFS(不推荐)
优化办法自然是避免每个元素都遍历,所以会想到BFS:
只统计每一层最远距离,如果某一层最大index覆盖了最后一个元素,则当前层就是最小跳数。
只需O(N)复杂度就能算出每一层的最大index
- 其实每到达一个点i,都会有nums[i]种跳跃选择,可以把每一轮的跳跃都加入队列中,然后再依次出队列,是可以得到最快到达终点的解的
- 其实是可以用优先队列的,不过我们自己保障了普通队列的顺序也行,按照跳跃距离从大到小入队列,对最终到达target是有帮助的
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int ans = 0;
boolean[] st = new boolean[n];
Deque<Integer> d = new ArrayDeque<>();
st[0] = true;
d.addLast(0);
while (!d.isEmpty()) {
int size = d.size();
while (size-- > 0) {
int idx = d.pollFirst();
//如果走到了最后一个位置
if (idx == n - 1)
return ans;
for (int i = idx + 1; i <= idx + nums[idx] && i < n; i++) {
if (!st[i]) {
st[i] = true;
d.addLast(i);
}
}
}
ans++;
}
return ans;
}
}
(3)贪心(注意一个细节)
- 要注意一个细节,就是 for 循环中,i < nums.length - 1,少了末尾。
- 因为开始的时候边界是第 0 个位置,steps 已经加 1 了。如下图,如果最后一步刚好跳到了末尾,此时 steps 其实不用加 1 了。如果是 i < nums.length,i 遍历到最后的时候,会进入 if 语句中,steps 会多加 1。
思路过程:
最开始遍历 i = 0, end = 0,因此 step 会进行 step ++,我们可以认为,这是开始起跳,因为必定会落下,因此跳跃次数 + 1
而 nums[0] 这个数限制了你只能在落脚在某个范围内,假如 nums[0] = 4,那么你只能选择落脚在 [1, 4] 位置,而如果到了边界,那么肯定是一次新的起跳,因此次数需要再 + 1
从 0 位置开始起跳,你落脚的必定是 [1, 4] 位置中能够跳得更远的,因为根据贪心思想,这样做能够尽可能的减少跳跃次数,因为更加接近最后一个位置
而在这个过程遍历 [1, 4] 过程中一直记录着最远位置 k,而你落地在 [1, 4] 之间,落地的那个点也就是 [1, 4] 之间最能够跳得远的那个位置,因此当到达边界的时候,将 end 更新为 k
注意:[1, 4] 跳得最远的位置必定不会在 [1, 4] ,因为如果在 [1, 4] ,那么表示根本就出不去 [1, 4] 这个圈
当然不会是 [4,1,1,1,0,1,2] 这种的,因为如果是这种的,压根过不去这个 0,因此必定第一次起跳必定能够跳出 [1, 4] 这个范围,比如 [4,1,1,1,1,1,0]
class Solution {
public int jump(int[] nums) {
//排除特例
if (nums == null || nums.length <= 1)
return 0;
int end = 0;
int maxPosition = 0;
int steps = 0;
//注意: i < nums.length - 1
//这里有个小细节,因为是起跳的时候就 + 1 了,如果最后一次跳跃刚好到达了最后一个位置
//那么遍历到最后一个位置的时候就会再次起跳,这是不允许的,因此不能遍历最后一个位置
for (int i = 0; i < nums.length - 1; i++) {
//找能跳的最远的
maxPosition = Math.max(maxPosition, nums[i] + i);
//遇到边界,就更新边界,并且步数加一
if (i == end) {
end = maxPosition;
steps++;
}
}
return steps;
}
}
避免最后一次跳跃刚好到达了最后一个位置,然后多余起跳,其实这样最好理解:
class Solution {
public int jump(int[] nums) {
//排除特例
if (nums == null || nums.length <= 1)
return 0;
int end = 0;
int maxPosition = 0;
int steps = 0;
for (int i = 0; i < nums.length; i++) {
//找能跳的最远的
maxPosition = Math.max(maxPosition, nums[i] + i);
//说明当前一步,再跳一步就到达了末尾
if (maxPosition >= nums.length - 1) {
steps++;
break;
}
//遇到边界,就更新边界,并且步数加一
if (i == end) {
end = maxPosition;
steps++;
}
}
return steps;
}
}
(4)贪心(最好理解)
以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点,这个范围内最小步数一定可以跳到,不用管具体是怎么跳的,不纠结于一步究竟跳一个单位还是两个单位。
class Solution {
public int jump(int[] nums) {
//排除特例
if (nums == null || nums.length <= 1)
return 0;
//记录跳跃的次数
int count=0;
//当前的覆盖最大区域
int end= 0;
//最大的覆盖区域
int max = 0;
for (int i = 0; i < nums.length; i++) {
//在可覆盖区域内更新最大的覆盖区域
max = Math.max(max, i + nums[i]);
//说明当前一步,再跳一步就到达了末尾
if (max >= nums.length-1){
count++;
break;
}
//走到当前覆盖的最大区域时,更新下一步可达的最大区域
if (i == end){
end = max;
count++;
}
}
return count;
}
}
3. 跳跃游戏 3
(1)BFS
- 用一个队列装入将要到达的下一步的位置下标,每个已经到达的地方就不需要再去继续遍历了
- 所以这里使用visited数组标记已经到达的地方,一定要清楚如果一个地方能到达数字0那么在第一次遍历也就是visited中位置还是false的时候就能找到0
- 就好比一条路要能去曹县那么踏上这条路必定会到,而第一次都没到后面再踏上这条路肯定也不能到。之后的工作就是能到的下标放进队列,直到找到数字0为止,没找到就是返回false;
class Solution {
public boolean canReach(int[] arr, int start) {
int len = arr.length;
boolean[] visited = new boolean[len];
Queue<Integer> queue = new LinkedList<>();
queue.add(start);
//遍历x + arr[x] 与 x - arr[x] 两个方向
while (!queue.isEmpty()) {
int x = queue.poll();
if (x + arr[x] < len && !visited[x + arr[x]]) {
visited[x + arr[x]] = true;
queue.add(x + arr[x]);
if (arr[arr[x] + x] == 0)
return true;
}
if (x - arr[x] >= 0 && !visited[x - arr[x]]) {
visited[x - arr[x]] = true;
queue.add(x - arr[x]);
if (arr[x - arr[x]] == 0)
return true;
}
}
return false;
}
}
(2)DFS
- 其实和广度优先搜索基本一样,只不过深度搜索是往下递归下一步去哪,而广度搜索采取队列存放下一步要到哪。核心的visited标记数组不能少,依然是走过的死胡同不能再走。
跳出递归的条件:
- index < 0 || index >=arr.length return false
- index 已经是访问过的下标 return false
- arr[index] == 0 return true
借助visited布尔数组来存储已经访问过的下标
class Solution {
public boolean canReach(int[] arr, int start) {
boolean[] visited = new boolean[arr.length];
return dfs(arr, visited, start);
}
public boolean dfs(int[] arr,boolean[] visited,int start){
if (start < 0 || start >= arr.length)
return false;
if(arr[start] == 0)
return true;
if (visited[start]) {
return false;
} else {
visited[start]=true;
}
return dfs(arr, visited, start + arr[start]) || dfs(arr, visited, start - arr[start]);
}
}
也可以这样写:
class Solution {
int[] arr;
boolean[] visited;
public boolean canReach(int[] arr, int start) {
this.arr = arr;
this.visited = new boolean[arr.length];
return dfs(start);
}
public boolean dfs(int start) {
if (start < 0 || start >= arr.length)
return false;
if (arr[start] == 0)
return true;
if (visited[start])
return false;
else
visited[start] = true;
return dfs(start + arr[start]) || dfs(start - arr[start]);
}
}