算法刷题 - 贪心02 (跳跃游戏Ⅱ + K 次取反后的最大数组和 + 加油站 + 分发糖果 + 柠檬水找零)

70 阅读8分钟

算法刷题博客封面.png

简介

  1. 跳跃游戏Ⅱ - LeetCode 45
  2. K 次取反后最大化的数组和 - LeetCode 1005
  3. 加油站 - LeetCode 136
  4. 分发糖果 - LeetCode 135
  5. 柠檬水找零 - LeetCode 860

题目 01 - 跳跃游戏Ⅱ

原题链接

LeetCode 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,当前步数内即可抵达
  • 需要记录 currMax 这一步的最大覆盖范围;和下一步可覆盖的最大范围 nextMax; 下一步范围由 i + nums[i] 更新
  • 每当抵达当前步数可达最远范围并且还未到边界,需要再跳下一步
  • 终止条件,当前可覆盖范围包含边界

图示

代码实现

class Solution {
    public int jump(int[] nums) {
        int n = nums.length;
        if(n == 1) {            // 检查输入
            return 0;
        }
        int currMax = 0;        // 当前最远可达
        int step = 0;           // 最少所需步数
        int totalMax = nums[0]; // 最远可达位置
        for(int i = 0; i <= currMax; i++) {
            totalMax = Math.max(totalMax, i + nums[i]);     // 更新最远可达
            if(currMax >= n - 1) {              // 此步内可以抵达最后一位
                return step;
            }
            if(i == currMax) {      // 已经抵达此步可达的最远位置
                step++;             // 再跳一步
                currMax = totalMax; // 更新最远可达为下一步最远可达
            }
        }
        throw new RuntimeException();
    }
}

执行结果

  • 时间复杂度: O(N)
  • 空间复杂度:O(1)

题目 02 - K 次取反后的最大化数组和

原题链接

LeetCode 1005 - K次取反后的最大数组和

题目描述

给定一个长度为 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]。

输入示例

思路

  • 越小的负数取反后越大,因此优先将所有负数取反;
  • 但是有可能将所有负数取反之后未消耗完 k 次数;但是对于 k 如果为偶数的话则可以忽略。由于负负得正,那么操纵同一个数字偶数次保持不变。
  • 而如果所有负数取反之后有剩余且 k % 2 后剩余 1; 则将此时最小值取反则能得到最大和;
    • 比如此时最小为一个负数,那么取反后该最小负数会变为负数
    • 反之如果最小为一个正数,由于该正数为最小,取反后使得和减少得也最少

图示

代码实现

class Solution {

    public int largestSumAfterKNegations(int[] nums, int k) {
        int n = nums.length;
        // 检查输入
        if (n == 0) {
            return 0;
        }
        // 按照从小到大排序
        Arrays.sort(nums);
        int curr = 0;
        // 优先将所有负数取反
        while (k > 0 && curr < n && nums[curr] < 0) {
            k--; 
            nums[curr] *= -1;
            curr++;
        }
        // 负数取反次数未消耗完
        if (k > 0 && k % 2 == 1) {
            Arrays.sort(nums);
            nums[0] *= -1;
        }
        return Arrays.stream(nums).sum();
    }
}

执行结果

  • 时间复杂度: O(NlogN)
  • 空间复杂度:O(1)

题目 03 - 加油站

原题链接

LeetCode 136 - 加油站问题

题目描述

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。

你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

输入示例

思路

  • 全局限制: 总油量需要大于等于总消耗量才能跑完一圈; 说明各个站点的加油站剩余油量 rest[i] 相加一定需要大于等于 0
    • 计算抵达每个加油站后的剩余油量为: gas[i] - cost[i]
    • i 从 0 开始累加 rest[i]得 curSum,若 curSum 小于零,[0, i]区间都不能作起始位置,因该区间任一起点到 i 都会断油,起始位置从 i + 1 算起,重新从 0 计算 curSum。
  • 局部最优: 当前累加 rest[i] 的和 currSum一旦小于 0,起始位置至少要是 i + 1。因为从i之前开始肯定不行。全局最优:找能跑一圈的起始位置。

图示

代码实现

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;           // 加油站数目
        int total = 0;                // 总体消耗油量
        int start = 0;                
        int tank = 0;        

        for (int i = 0; i < n; i++) {
            total += gas[i] - cost[i];  // 标记总体消耗油量是否足够支撑走完全部站点
            tank += gas[i] - cost[i];   // 标记当前站点起步的话油箱是否足够
            if (tank < 0) {             // 无法抵达下一个站点
                start = i + 1;          // 更新起始加油站下标
                tank = 0;
            }
        }

        return total < 0 ? -1 : start;
    }
}

执行结果

  • 时间复杂度: O(N)
  • 空间复杂度:O(1)

题目 04 - 分发糖果

原题链接

LeetCode 135 - 分发糖果

题目描述

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

输入示例

思路

  1. 只要右边评分比左边大,右边的孩子至少要多一个糖果
  2. 规则:相邻的孩子中,评分最高的有孩子获得比左孩子更多的糖果
  3. 从左往右遍历,如果 ratings[i] > ratings[i - 1] 为了保证局部最优,分配糖果总数目最少
    那么分配的糖果数目 candyVec[i] = candyVec[i - 1] + 1 比左侧多一个
  4. 再确定左孩子大于右孩子的情况; 如果 ratings[i] > rating[i + 1] 那么此时 candyVec[i]
    要保证第 i 个小孩的糖果数量既大于左边的也大于右边的,存在两种可能:
  • candyVec[i + 1] + 1 从右边这个加 1 得到的糖果数量
  • candyVec[i] 由于第一次分配已经大于左侧跟右侧
  1. 保证第 i 个小孩的糖果数量既大于左边的也大于右边的
    当取 max(candyVec[i + 1] + 1, candyVec[i]) 中最大的糖果数量,
    才能保证 candyVec[i - 1] 的糖果更多,也比右边 candyVec[i + 1] 糖果多

代码实现

class Solution {
    
    public int candy(int[] ratings) {
        int n = ratings.length;
        int[] cnt = new int[n];
        cnt[0] = 1;      // 最少分配一个糖果
        for(int i = 1; i < n; i++) {    
            // 从左往右,评分比其左侧高的孩子的糖果数目得到更多;
            //否则保证局部糖果数目最小,分配 1
            cnt[i] = (ratings[i] > ratings[i - 1]) ? cnt[i - 1] + 1 : 1; 
        }
        // 从右往左,评分比其右侧高的孩子要分得比其右侧多; 由于本身糖果数目有可能已经比右侧多;
        // 为了保证局部糖果数目最小,分配 max(candyVec[i + 1] + 1, candyVec[i]) 中得较大值
        for(int i = ratings.length - 2; i >= 0; i--) {  
            if(ratings[i] > ratings[i + 1]) {
                cnt[i] = Math.max(cnt[i], cnt[i + 1] + 1);
            }
        }
        return Arrays.stream(cnt).sum();
    }
}

执行结果

  • 时间复杂度: O(N)
  • 空间复杂度:O(N)

题目 05 - 柠檬水找零

原题链接

LeetCode 860 - 柠檬水找零

题目描述

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

输入示例

思路

  1. 由于 5 元最通用,因此找零钱的时候找 5 的优先级最低;优先使用 20, 10找零

  2. 付钱找零情况:

    • 客户付 5 元则无需找零
    • 付 10 元则需要消耗一张 5 元
    • 付 20 元,由于 5 元更加通用;优先找零 1 张 10 元和 1 张 5元;否则找 3 张 5 元
  3. 若剩余的 5 元数目 < 0 则无法完成正确的找零

代码实现

class Solution {

    public boolean lemonadeChange(int[] bills) {
        int five = 0;   // 5 元的剩余零钱数目
        int ten = 0;    // 10 元的剩余零钱数目
        
        for(int b : bills) {
            if(b == 5) {            // 付 5 块不用找零
                five++;
            } else if (b == 10) {   // 付 10 块
                ten++;  
                five--;             // 找一张 5 块
            } else {                // 付 20
                if(ten >= 1) {      // 有 10 块零钱先找 10 块
                    ten--;
                    five--;
                } else {
                    five -= 3;      // 否则找 3 张 5 块
                }
            }
            if(five < 0) {  // 不够支付
                return false;
            }
        }
        return true;
    }
}

执行结果

  • 时间复杂度: O(N)
  • 空间复杂度:O(1)