LeetCode刻意练习--跳跃游戏

题目:
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。

方法一: 回溯法

有一组数据超时。
在这里插入图片描述

  1. 遍历每个元素,对于每个元素i,先跳最大的步数nums[i],到达下一个元素next。
  2. 如果能够跳出这个数组(nums[i]+i>=nums.length-1),那么它一定可以跳到这个数组的最后一个元素。如果不能够跳出这个数组:
    (1)next的值如果为0,则这条路是不可行的,则进行回溯,注意:是进行回溯,不能直接返回false。
    (2)next的值如果不为0,则从该结点开始进行递归
代码实现如下:
    public boolean canJump(int[] nums, int start) {
        // 基本情况,反复递归,如果跳到最后一个元素
        if (start == nums.length - 1)
            return true;
        for (int i = nums[start]; i > 0; i--) {
            if (i + start >= nums.length - 1)
                return true;
            if (nums[start + i] != 0) {
                //这里是回溯的处理方式!
                if (canJump(nums, start + i))
                    return true;
            }
        }
        return false;
    }

当我们对方法一进行递归树的分析,
输入: [3,2,1,0,4]

元素 3 2 1 0 4
角标 0 1 2 3 4

注c(i)==canJump(nums,i)
在这里插入图片描述
我们可以看出递归树有很多重复的子问题,因此我们对它进行剪枝,因为 C(3)已经被计算过不能走了,因此我们将C(2)中的C(3)剪去,而C(2)不能到达C(0),因此C(2)也不能走到。C(1)中的C(2)和C(3)之前都已经证明不能走,因此没有必要再进行计算。

根据上述优化,我们引入第二种方法。

方法二:动态规划记忆法(自上而下)

在这里插入图片描述
利用一个Demo数组,数组元素初始化为0,对所有情况进行标记,如果能够到达最后一个元素,则标记为1,不能到达标记为-1。
代码修改:在return前要先赋值,在遍历前要先看在记忆里有没有访问过


    public boolean canJump(int[] nums) {
        int[] demo = new int[nums.length];
        for (int i = 0; i < nums.length; i++)
            demo[i] = 0;
        return helper(nums, 0, demo);
    }

    public boolean helper(int[] nums, int start, int[] demo) {
        if (start == nums.length - 1) {
            demo[start] = 1;//在return前要先赋值,
            return true;
        }
           //在遍历前要先看在记忆里有没有访问过
        if (demo[start] == 0) {
            for (int i = nums[start]; i > 0; i--) {
                if (i + start >= nums.length - 1) {
                    demo[start] = 1;//在return前要先赋值,
                    return true;
                }

                if (nums[start + i] != 0) {
                    if (helper(nums, start + i, demo)) {
                        demo[start] = 1;//在return前要先赋值,
                        return true;
                    }
                }
            }
            demo[start] = -1;//在return前要先赋值,
            return false;
        }
        return demo[start] == 1;
    }

方法三:动态规划制表法(自下而上)

在这里插入图片描述
记忆法(自上而下)是从左往右遍历元素看是否是能够到达最后一个元素。那么表格法(自下而上)就是从右边到左边去遍历。

  1. 最右边一个肯定是,自身能够到达自身(can)
  2. 左边的是不是,取决于它能不能跳到它右边的can,也就是说在它能跳的范围之内是不是有can
  3. 如果它能够跳到can,则说明它也是can,否则,它是can’t
  4. 依次往左,看最左边一个是不是can
    public boolean canJump(int[] nums) {
        int[] table = new int[nums.length];
        table[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 (table[j] == 1)
                    table[i] = 1;
            }
        }
        return table[0] == 1;
    }

我们可以将上述问题再进行优化,本来看一个元素是不是can,是遍历它右边的每个元素看有没有can,现在我们不用去遍历它右边的每个元素,而是利用一个变量closed跟踪它右边离它最近的can。于是我们引入贪心算法。

方法四:贪心算法

这个解法是贪心算法的原因是它右边的元素已经处理完,离它最近的can也已经被我们用closed 跟踪了,因此,我们不用再去考虑我们已经解决的右边元素。而动态规划还要继续遍历右边的元素以寻找can。这就是贪心算法和动态规划最主要的区别。贪心算法不用再去考虑已经解决的子问题。关于贪心算法
在这里插入图片描述

    public boolean canJump(int[] nums) {
        int[] table = new int[nums.length];
        table[nums.length - 1] = 1;
        int closed = nums.length - 1;//记录右边can元素
        for (int i = nums.length - 1; i >= 0; i--) {
            if (i + nums[i] >= closed) {
                table[i] = 1;
                closed = i;//跟踪最左边的can元素
            }

        }
        return table[0] == 1;
    }

我们可以将算法再进行优化,因为closed是用来标记最左边的can,因此,只要closed是0,则第0个元素是最左边的can。因此,我们有方法五。

方法五:贪心算法优化

在这里插入图片描述

    public boolean canJump(int[] nums)
    {
        int closed=nums.length-1;
        for (int i=nums.length-2;i>=0 ;i-- ) {
            if(i+nums[i]>=closed)
                closed=i;
        }
        return closed==0;
    }
发布了49 篇原创文章 · 获赞 2 · 访问量 857

猜你喜欢

转载自blog.csdn.net/qq_43720551/article/details/105166418