LeetCode 👉 HOT 100 👉 跳跃游戏 - 中等题

426 阅读5分钟

题目

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

示例1

输入:nums = [2,3,1,1,4]

输出:true

解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

示例2

输入:nums = [3,2,1,0,4]

输出:false

解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

思路

数组类的算法题,肯定少不了遍历;那么就开始遍历吧

DFS解法

对于第 i 位置上的元素 nums[i],会想到什么?

按照题目,大部分人的第一反应会是:下一步是哪?

按照题意,第 i 位置上,能够到达的位置为 i + 1i + 2 ... i + nums[i]

那么接下来就会想,那应该 到哪个位置是最合适的呢?

这个时候,上一步这个问题是无法判断出来的,那怎么办?

没办法,只有每一个都去尝试一遍

上述的过程,整理一下,其实就是 DFS(深度优先搜索) 算法的思想,递归下去,回溯上来,不过这道题的回溯很少,如果仅仅采用 DFS,算法的时间复杂度会很高,下面就会看得到:

第一版 DFS 代码如下

    /**
     * @param {number[]} nums
     * @return {boolean}
     */
    var canJump = function (nums) {

        // 定义标识,是否到达了最后一个位置
        let ans = false;

        let n = nums.length;
        /**
         * 
         * @param {Number} idx 当前起跳位置
         * @returns null
         * 
         * @description 传递一个起跳位置,从该位置开始,跳到所有可能的下一个位置
         */
        const jump = (idx) => {

            // 如果当前位置比数组长度大,不合法,不需要继续进行,第一次剪枝
            if (idx >= n) return;

            // 如果当前位置就是最后一个位置,或者已经跳到最后一个位置,不需要继续进行,第二次剪枝
            if (idx === n - 1 || ans) {
                ans = true;
                return;
            }
            // 当前能跳的最大距离
            let loop = nums[idx];

            // 如果当前最大距离 比 idx -> n 之间的距离大,那么肯定能到达最后一个位置,第三次剪枝
            if (loop >= (n - idx)) {
                ans = true;
                return;
            }

            // 依次,跳转到下一个位置,继续调用 jump 递归
            for (let i = 1; i <= loop; i++) {
                jump(idx + i);
            }
        }

        // 从位置0开始,启动递归函数
        jump(0);

        return ans;
    };

上面的解法,时间复杂度极高,提交 leetCode 的时候,直接显示超出时间限制了😭😭😭

思考了一下:其实还有一个很重要的 剪枝 操作忘记了,对于位置 i,其实只需要进行一次调用 jump 函数就可以了,后面的都是重复操作;

那么可以使用一个辅助数组,用于标记 i 是否进行过调用 jump,

第二版 记忆DFS 代码如下

增加了三行代码,都是 `vis` 相关,但是将时间复杂度优化到了接近 `O(n)`,这就是空间换时间了
    /**
     * @param {number[]} nums
     * @return {boolean}
     */
    var canJump = function (nums) {

        // 定义标识,是否到达了最后一个位置
        let ans = false;

        let n = nums.length;

        // 辅助函数,用于判断 i 是否经过计算
        let vis = new Array(n).fill(false);
        /**
         * 
         * @param {Number} idx 当前起跳位置
         * @returns null
         * 
         * @description 传递一个起跳位置,从该位置开始,跳到所有可能的下一个位置
         */
        const jump = (idx) => {

            // 如果当前位置比数组长度大,不合法,不需要继续进行,第一次剪枝
            // 增加一个剪枝操作,vis[i] 计算过
            if (idx >= n || vis[idx]) return;

            vis[idx] = true;

            // 如果当前位置就是最后一个位置,或者已经跳到最后一个位置,不需要继续进行,第二次剪枝
            if (idx === n - 1 || ans) {
                ans = true;
                return;
            }
            // 当前能跳的最大距离
            let loop = nums[idx];

            // 如果当前最大距离 比 idx -> n 之间的距离大,那么肯定能到达最后一个位置,第三次剪枝
            if (loop >= (n - idx)) {
                ans = true;
                return;
            }

            // 依次,跳转到下一个位置,继续调用 jump 递归
            for (let i = 1; i <= loop; i++) {
                jump(idx + i);
            }
        }

        // 从位置0开始,启动递归函数
        jump(0);

        return ans;
    };

动态规划

上面的解法,思考的主要方向是 下一步是哪

那么对于位置 i,思考一下,当前 i 是能够到达的么?如果能够到达,条件是什么呢?

不难想出:对于 i 如果是能够到达,必定在 i 之前存在一个 j,使得 j + nums[j] >= i

那么就可以使用动态规划去解决这道题:

动态规划 解法代码如下

    /**
     * @param {number[]} nums
     * @return {boolean}
     */
    var canJump = function(nums) {
        let n = nums.length;

        // 新建一个dp数组,dp[i] 表示 i 位置能否到达
        let dp = new Array(n).fill(false);

        // 初始条件,位置 0 肯定能到达
        dp[0] = true;


        for(let i = 1; i < n; i++) {
            // 遍历小于 i 的元素
            for(let j = 0; j < i; j++) {
                // 如果当前 dp[j] 为 true,表示dp[j] 可到达
                // 如果 nums[j] + j >= i,表示 dp[j] 可到达
                // 找到一个就可以了,中断循环 break
                if(dp[j] && nums[j] + j >= i) {
                    dp[i] = true;
                    break;
                }
            }
        }

        // 题目所求即为 dp数组最后一个元素的值
        return dp[n - 1];
    };

发现没有,相比较于第一种算法,动态规划的解法代码量少了很多!!!

贪心

贪心算法的思想是,每次都选当前看起来最好的那个选择,不管整体影响,那么再来看这道题

对于位置 i 的元素能否到达,是不是也可以用一个动态变化的 当前最大能跳多远 -> jumpMax 的变量来判断呢?

比如,对于位置 2jumpMax 如果比 2 小,则位置 2 不能到达,否则能到达

那么在不能到达的情况下,还需要继续判断位置 2 以后的元素么?

答案是不需要的,自己体会下,比如对于 [0, 0, 1, 3],当前 i == 1; jumpMax == 0, 位置 1 显然不能到达,位置 1 以后的也不能达到

贪心算法代码如下

    /**
     * @param {number[]} nums
     * @return {boolean}
     */
    var canJump = function(nums) {
        let n = nums.length;

        // 当前最大能走多远,初始时为 nums 第一个元素的值
        let jumpMax = nums[0];

        for(let i = 0; i < n; i++) {
            // 如果,当前最大能走的距离,比当前的索引 i 还少,则 位置 i 无法到达
            if(jumpMax < i) return false;

            // 否则更新一下当前最大能走的距离 -> i + nums[i] 为 i 位置上当前最远能走的距离
            jumpMax = Math.max(jumpMax, i + nums[i]);

            // 如果当前最大能走的距离,大于数组的长度,则最后一个位置能到达
            if(jumpMax >= n - 1) return true;
        }

        // 上面的遍历,每个元素都能到达,最后一个元素也能到达
        return true;
    };

小结

上面三种解法,一个比一个代码量少,最后一个的时间复杂度,降到了最低的 O(n),效率最高,但同时也是最难想到的一种解法

算法这个东西,你不练,它就不理你~

LeetCode 👉 HOT 100 👉 跳跃游戏 - 中等题

合集:LeetCode 👉 HOT 100,有空就会更新,大家多多支持,点个赞👍

如果大家有好的解法,或者发现本文理解不对的地方,欢迎留言评论 😄