代码随想录Day35

今天继续学习贪心算法解决相关问题

45.跳跃游戏||

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

0 <= j <= nums[i] 
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

思路:

1.相比起Day34的跳跃游戏,本题保证了能够到达终点,反问我们需要跳几次。那么我们的问题就转化为:什么时候才要跳一次

2.首先能想到的是肯定和上一题一样,我们不需要关心本次我们跳几步,只需要考虑当前能跳跃的范围,如果能覆盖到终点即可。那么顺着这个思路想,如果我们当前位置最远的跳跃覆盖范围不能到达终点,那么我们就只能跳一步以期望扩大跳跃的覆盖范围。因此遍历时需要存储当前位置的最大覆盖范围,如果接下来遍历时到达了记录的最大覆盖范围却没有到达终点,我们就需要跳一步。

既然是跳一步,那么我们直接跳到我们所记录的当前位置最远的范围嘛?显然不是,题目示例中的那个例子已经显示了。因此我们还需要在遍历的过程中记录下一个位置的跳跃覆盖范围,并且要取最大值。每当我们需要跳一步时,我们一定是跳到下一个位置跳跃覆盖范围最大的那一个点上,这样才能保证用最少的跳跃次数覆盖更大的范围。同样的我们不需要关心我们这一步具体跳了几格,我们只需要知道我们当前存储的跳跃范围能够跳到这个点,并且这个点对应的跳跃范围是当前存储的跳跃范围内最大的就行

class Solution {
public:
    int jump(vector& nums) {
        if(nums.size() == 1) return 0;
        int curDis = 0;//记录当前的覆盖范围
        int result = 0;//存储最终结果,即跳跃的步数
        int nextDis = 0;//记录下一步的覆盖范围

        for(int i = 0; i < nums.size() - 1; i++){
            nextDis = max(nextDis, nums[i] + i);
            //走到了当前覆盖的最大范围但是还没覆盖到终点,需要走一步以扩大覆盖范围
            if(i == curDis){
                //当前没有覆盖到终点
                if(curDis < nums.size() - 1){
                    result++;
                    curDis = nextDis;
                    //下一步已经覆盖到了终点
                    if(nextDis >= nums.size() - 1){
                        break;
                    }
                }
                //当前的覆盖范围已经超过了终点,不需要再走了
                else{
                    break;
                }
            }
        }
        return result;
    }
};

启发:

1.注意本题中初始距离为0,即最初我们是没有覆盖范围的,这是为什么呢?个人理解的想法是:需要想到,除非数组只有一个元素我们不需要跳,那么我们至少都是需要跳一次才能到达终点的,有可能从第一个点开始我们直接就能覆盖到终点,但极有可能忽略这种情况导致最终结果为0。因此我们最初就把结果和初始覆盖范围设置为0,在第一轮进行for循环的时候才把第一个位置的覆盖范围存储到CurDis中。

2.个人觉得本题的解法有一种类似嵌套for循环但又没有嵌套的感觉,主要还是通过存储当前范围和下一步的最大覆盖范围,并且让i一直走到当前范围所覆盖的最远距离时才进行一次判断,这种循环方式需要进一步加深理解。

1005.K次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。

以这种方式修改数组后,返回数组 可能的最大和 。

示例 1:

输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。
示例 2:

输入:nums = [3,-1,0,2], k = 3
输出:6
解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。

思路:

1.要使得最终的数组和最大,肯定优先取反小的负数(即绝对值大的负数)变成正数。最初的思路是直接对数组进行排序,然后从头开始遍历将负数进行取反直到取了k次或者负数全部被取反成正数。初步的思路是正确的,但在这之后的操作稍显繁琐:如果k取完了但还是有负数,这种情况比较好处理,我们直接求数组和即可;如果负数全部变成了正数,那么我们仍要从遍历结束的位置开始继续往前遍历一点,找到最小的正数把剩下的k次取反用完。

2.进一步优化之后,我们可以在最初对数组排序时就按照元素的绝对值从大到小进行排序,这样从头开始遍历遇到负数时就取反,这样能保证把绝对值大的负数变成正数,并且如果负数全部变成了正数后k还有剩余,我们就不需要再继续遍历,直接将数组中最后的一个元素把剩余k次取反用完即可。

class Solution {
public:
    static bool cmp(int a, int b){
        return abs(a) > abs(b);
    }
    int largestSumAfterKNegations(vector& nums, int k) {
        sort(nums.begin(), nums.end(), cmp);//按照绝对值从大到小排序
        for(int i = 0; i < nums.size(); i++){
            //遇到负数且还有取反次数,进行取反
            if(nums[i] < 0 && k > 0){
                nums[i] *= -1;
                k--;
            }
            //没有取反次数了就直接退出
            if(k == 0) break;
        }
        //如果还有剩余的取反次数,且为奇数,直接将数组中最后一个元素取反一次即可,效果和奇数次取反是一样的
        if(k % 2 == 1) nums[nums.size() - 1] *= -1;
        int result = 0;
        for(int i = 0; i < nums.size(); i++){
            result += nums[i];
        }
        return result;
    }
};

启发:

1.本题实质上用了两次贪心思想,第一次局部最优:找绝对值最大的负数进行取反,保证全局数组和最大。第二次局部最优:如果k还有剩余,找绝对值最小的正数进行取反消耗完剩余的k,保证全局数组和最大

2.本题虽然思路好想,但代码优化的思想却很难想到,一个是直接根据绝对值从大到小进行排序,个人一开始是直接按照思路1中的初步思路去做了,最后还是稍微复杂了些许;一个是k如果还有剩余进行取反的时候,奇数次取反和一次取反的效果是一样的,偶数次取反和不取反的效果是一样的。当k还剩余的比较多时我们没有必要真的重复进行k次取反。

你可能感兴趣的:(代码随想录,数据结构,算法,leetcode,c++,贪心算法)