贪心算法及Jump Game系列题详解

本博文所有的代码均可在
https://github.com/Hongze-Wang/LeetCode_Java
https://github.com/Hongze-Wang/LeetCode_Python
中找到,欢迎star。

贪心算法属于比较难的算法,一般用于求解最优解或者极限情况下判断可能性。贪心和动态规划的区别在于,贪心算法的解题过程中会展现出最优子结构,子问题的最优解组合构成了全局的最优解,而动态规划则是考虑所有子问题,针对这些子结构求解,从中选取出最优解。

贪心算法有两个重要组成部分:

  1. 贪心策略
  2. 最优子结构
贪心策略的选取将对贪心算法能否得到最优解起到了决定性的作用。最优子结构指的是,大问题分解成小问题时,使用拟定好的贪心策略一样能得到小问题的最优解。

详见Greedy Analysis Strategies,摘自普林斯顿大学算法课件。
 
贪心算法动态规划的共同点在于分治思想,大问题拆解成小问题,从求解小问题过程中得到大问题的解。

贪心算法动态规划的不同点在于最优子结构即贪心算法相当于认定了贪心策略下的解能得到最优解;而动态规划则穷举了所有的解的可能性,从中经过比较得到了最优解。

因此贪心算法不一定能保证最优解,但可以保证一个还不错的解。动态规划一定能保证最优解,但因为它要穷举所有可能性,计算复杂度会很高,在很多场景下无法使用。

注:如果贪心策略是解决问题的唯一策略,那么贪心算法也一定能取得最优解,而且比动态规划复杂度要小得多。

Jump Game

来自LeetCode的一组题,我们这里只介绍贪心算法,其中55题既可以使用贪心解也可以使用动态规划,它是一个贪心算法复杂度低于动态规划的一个实例。官方注释是英文的,因此我加的注释也是英文的,如果有看不懂的地方可以在评论区提问。

  1. Jump Game (Medium)
  2. Jump Game II (Hard)
  3. Jump Game III (Medium)
55. Jump Game (Medium)

Given an array of non-negative integers nums, you are initially positioned at the first index of the array.
Each element in the array represents your maximum jump length at that position.
Determine if you are able to reach the last index.

Example 1:
Input: nums = [2,3,1,1,4]
Output: true
Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2:
Input: nums = [3,2,1,0,4]
Output: false
Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.

Constraints
1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105

贪心策略:选取最大的步数,从结束点逆推到起始点,看能不能回到起始点,如果能,自然也能从起始点到达结束点。选取最大步数在程序中体现的是i+nums[i],如果i+nums[i]>=lastPos,那么从i是能到达lastPos的。

// 55. Jump Game

// The Solution of this question is very detail and inspring
// I recommend you to read it for more info and idea: https://leetcode.com/problems/jump-game/solution/

// Usually, solving and fully understanding a dynamic programming problem is a 4 step process:
// Start with the recursive backtracking solution
// Optimize by using a memoization table (top-down dynamic programming)
// Remove the need for recursion (bottom-up dynamic programming)
// Apply final tricks to reduce the time / memory complexity

// Approach 1: Backtracking (Brute Force) 这里没有回溯过程 所以我觉得不能称作回溯法 只是单纯的DFS而已
// check every pos from curr pos and all the reachable
// Time Limit Exceeded
class Solution {
    public boolean canJumpFromPosition(int pos, int[] nums) {
        if(pos == nums.length - 1) {
            return true;
        }
        int fur = Math.min(pos + nums[pos], nums.length-1);
        for(int next = pos+1; next<=fur; next++) {
            if(canJumpFromPosition(next, nums)) {
                return true;
            }
        }
        return false;
    }
    public boolean canJump(int[] nums) {
        return canJumpFromPosition(0, nums);
    }
}

// Approach 1: Backtracking (Brute Force) Optimized
// check every pos from curr pos and all the reachable from right to left
// this means we always try to make the biggest jump such that we reach the end as soon as possible (greedy policy)
// Time Limit Exceeded
class Solution {
    public boolean canJumpFromPosition(int pos, int[] nums) {
        if(pos == nums.length - 1) {
            return true;
        }
        int fur = Math.min(pos + nums[pos], nums.length-1);
        for(int next = fur; next>pos; next--) {
            if(canJumpFromPosition(next, nums)) {
                return true;
            }
        }
        return false;
    }
    public boolean canJump(int[] nums) {
        return canJumpFromPosition(0, nums);
    }
}

// Approach 2: Dynamic Programming Top-down
// It relies on the observation that once we determine that a certain index is good / bad, this result will never change. 
// This means that we can store the result and not need to recompute it every time.
// memoization

enum Index {
	GOOD, BAD, UNKNOW
}

public class Solution {
    Index[] memo;
    
    public boolean canJumpFromPosition(int pos, int[] nums) {
        if(memo[pos] != Index.UNKNOW) {
            return memo[pos] == Index.GOOD ? true : false;
        }
        int fur = Math.min(pos+nums[pos], nums.length-1);
        for(int next=pos+1; next<=fur; next++) {
            if(canJumpFromPosition(next, nums)) {
                memo[pos] = Index.GOOD;
                return true;   
            }
        }
        memo[pos] = Index.BAD;
        return false;
    }
    
    public boolean canJump(int[ ] nums) {
        memo = new Index[nums.length];
        for(int i=0; i<memo.length; i++) {
            memo[i] = Index.UNKNOW;
        }
        memo[memo.length-1] = Index.GOOD;
        return canJumpFromPosition(0, nums);
    }
}

// Top down dp
public class Solution {
    public boolean canJumpFromPostion(int pos, int[] nums, int[] memo) {
        if(memo[pos] != 0) {
            return memo[pos] == 1 ? true : false;
        }
        int fur = Math.min(pos+nums[pos], nums.length-1);
        for(int next=pos+1; next <= fur; next++) {
            if(canJumpFromPostion(next, nums, memo)) {
                memo[pos] = 1;
                return true;
            }
        }
        memo[pos] = 2;
        return false;
    }

    public boolean canJump(int[] nums) {
        int[] memo = new int[nums.length];
        memo[nums.length-1] = 1;
        return canJumpFromPostion(0, nums, memo);
    }
}

// Approach 3: Dynamic Programming Bottom-up
// Top-down to bottom-up conversion is done by eliminating recursion
// The recursion is usually eliminated by trying to reverse the order of the steps from the top-down approach.

enum Index {
    GOOD, BAD, UNKNOW
}

public class Solution {
    public boolean canJump(int[] nums) {
        Index[] memo = new Index[nums.length];
        for(int i=0; i<nums.length; i++) {
            memo[i] = Index.UNKNOW;
        }
        memo[nums.length-1] = Index.GOOD;
        for(int i=nums.length-2; i>=0; i--) {
            int fur = Math.min(i+nums[i], nums.length-1);
            for(int j=i+1; j<=fur; j++) {
                if(memo[j] == Index.GOOD) {
                    memo[i] = Index.GOOD;
                    break;
                } 
            }
        }
        return memo[0] == Index.GOOD;
    }
}

// Bottom up dp
public class Solution {
    public boolean canJump(int[] nums) {
        int[] memo = new int[nums.length];
        memo[nums.length-1] = 1;
        
        for(int i=nums.length-2; i>=0; i--) {
            int fur = Math.min(i+nums[i], nums.length-1);
            for(int j=i+1; j<=fur; j++) {
                if(memo[j] == 1) {
                    memo[i] = 1;
                    break;
                }
            }
        }
        return memo[0] == 1;
    }
}

// Approach 4: Greedy
// Greedy Policy 
// try to make the biggest jump such that we reach the start from the end as soon as possible
public class Solution {
    public boolean canJump(int[] nums) {
        int lastPos = nums.length-1;
        for(int i=nums.length-1; i>=0; i--) {
            if(i+nums[i] >= lastPos) {
                lastPos = i;
            }
        }
        return lastPos == 0;
    }
}

动态规划再怎么优化都是O(n^2),但贪心是O(n)复杂度确实要小得多。

45. Jump Game II (Hard)

Given an array of non-negative integers nums, you are initially positioned at the first index of the array.
Each element in the array represents your maximum jump length at that position.
Your goal is to reach the last index in the minimum number of jumps.
You can assume that you can always reach the last index.

Example 1:
Input: nums = [2,3,1,1,4]
Output: 2
Explanation: The minimum number of jumps to reach the last index is 2. Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2:
Input: nums = [2,3,0,1,4]
Output: 2

Constraints:
1 <= nums.length <= 3 * 104
0 <= nums[i] <= 105

这道题比上一道考察贪心的倾向更明显,因为它求的是最小步数,是个求最优解问题。贪心策略同上,也是每次都选取能走的最大步数,最后一次走的时候,如果i+nums[i] > nums.length-1而溢出,那么i == pos不会成立,因而不会再计次数。

// 45. Jump Game II
// greedy: take the highest step as you can in every jump

class Solution {
    public int jump(int[] nums) {
        if(nums.length == 1) return 0;
        
        int pos = 0, reachable = 0, count = 0;
        for(int i=0; i<nums.length-1; i++) { // last elem do not need consider
            reachable = Math.max(reachable, i+nums[i]);
            if(i == pos) {
                pos = reachable;
                count++;
            }
        }
        return count;
    }
}

// reachable store the current highest position it can reach
// pos store the last hight postion it can reach
// if i == pos means that i can be reach
// update pos = reachable (currennt highest postion it can rech) greedy policy
// count increase by one

1306. Jump Game III (Medium)

Given an array of non-negative integers arr, you are initially positioned at start index of the array. When you are at index i, you can jump to i + arr[i] or i - arr[i], check if you can reach to any index with value 0.

Notice that you can not jump outside of the array at any time.

Example 1:
Input: arr = [4,2,3,0,3,1,2], start = 5
Output: true
Explanation:
All possible ways to reach at index 3 with value 0 are:
index 5 -> index 4 -> index 1 -> index 3
index 5 -> index 6 -> index 4 -> index 1 -> index 3

Example 2:
Input: arr = [4,2,3,0,3,1,2], start = 0
Output: true
Explanation:
One possible way to reach at index 3 with value 0 is:
index 0 -> index 4 -> index 1 -> index 3

Example 3:
Input: arr = [3,0,2,1,2], start = 2
Output: false
Explanation: There is no way to reach at index 1 with value 0.

Constraints:
1 <= arr.length <= 5 * 104
0 <= arr[i] < arr.length
0 <= start < arr.length

这道题虽然也放到这里了,但不是贪心,因为每次跳法只有两种,这两种跳法无法判断谁最优,而且也不是求最优解问题。DFS或者BFS都可以做,下面的解法使用了一个trick,使用取负来标志一个元素被访问过。

// 1306. Jump Game III

// DFS
class Solution {
    public boolean canReach(int[] arr, int start) {
        if(start < 0 || start >= arr.length || arr[start] < 0) return false;
        if(arr[start] == 0) return true;
        arr[start] = -arr[start]; // mark arr[start] as visited
        return canReach(arr, start+arr[start]) || canReach(arr, start-arr[start]);
    }
}


// BFS
class Solution {
    public boolean canReach(int[] arr, int start) {
        int n=arr.length;
        Queue<Integer> q = new LinkedList();
        q.add(start);
        
        while(!q.isEmpty()) {
            int node = q.poll();
            if(arr[node] == 0) return true;
            if(arr[node] < 0) continue;
            
            if(node + arr[node] < n) {
                q.offer(node + arr[node]);
            }
            if(node - arr[node] >=0) {
                q.offer(node - arr[node]);
            }
            arr[node] = -arr[node];
        }
        return false;
    }
}

你可能感兴趣的:(数据结构与算法分析,LeetCode刷题总结,贪心算法,动态规划,算法)