贪心算法典例 - 分发糖果&加油站 - LeetCode

贪心算法每日两题

贪心算法没有固定的解题模版,题目灵活多变,此处笔记不再划分题型,只选取经典题型帮助理解

一、分发糖果

题目链接: 135.分发糖果
题目要求:

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的最少糖果数目

提示:

n == ratings.length
1 <= n <= 2 * 104
0 <= ratings[i] <= 2 * 104

示例

输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。

解题思路:
首先读题,根据题目和示例可以总结出以下规则:

  • 每个孩子至少分配1个糖果
  • 相邻孩子中评分更高的孩子一定比评分低的孩子有更多糖果
  • 相邻孩子的评分如果相同,所得到的糖果数可以不相同(例如对评分 [1, 2, 2, 3], 糖果总数最少的情况为 [1, 2, 1, 2]
  • 要求找出最少所需的糖果总数

读题以后发现简单地遍历数组,看到后一个元素更大就加一,更小就减一的方法显然是不行的,因为这种方法将视野限制在当前的两个元素上,如果出现连续递减的情况,则糖果数很有可能小于1。为了解决这个问题,我们可以将视野扩展的局部区间,寻找最长连续递增(递减)子序列,子区间左端(右端)端点的糖果数一定是1,其余节点糖果数依次递增(递减)1。但这种思路只考虑到了某一个单调区间的取值问题,无法处理其余部分的取值,详细地说就是递增区间右侧的第一个元素的取值既不能简单地通过减一得到(它有可能比周围的元素都小,可以取0),也不能直接赋值为0(它有可能大于右侧的元素,必须比右侧元素大1)。

事实上,根据分析可以发现,糖果数序列中除端点以外的所有元素必须满足左右两重相对关系。既然如此我们也可以将确定糖果数的步骤分为保证左侧相对关系正确和右侧相对关系正确两部分,当所有除端点以外的所有元素均满足两重相对关系时,所得序列一定是合理的。这两个过程均可以通过一重for循环遍历数组来实现。

首先初始化糖果数数组nums[]中的每一个元素为1,以免出现最小糖果数小于1的情况。

然后保证右半部分的关系正确:
从前向后循环,保证如果右侧评分大于左侧评分,则右侧孩子糖果数一定更多

for(int i = 1; i < ratings.size(); i++) {
    if(ratings[i] > ratings[i-1]) nums[i] = nums[i-1] + 1;
}

保证左半关系同时正确:
注意本次遍历所确定的元素的是比较的元素对右侧的nums[i+1],所以每一次比较都要保证右侧的元素已经比较过了,为保证这一点我们必须从右向左遍历数组。
注意:

  • nums[i] + 1 是 nums[i+1] 满足左侧条件的下限
  • nums[i+1] 是 nums[i+1] 满足右侧关系的下限
  • 两个下限必须同时满足,所以取两者最大值
for(int i = ratings.size() - 2; i > -1; i++) {
    if(ratings[i+1] > ratings[i]) {
        nums[i+1] = max(nums[i] + 1, nums[i+1]);
    }
}

代码示例:

class Solution {
public:
    int candy(vector<int>& ratings) {
        int ans = 0;
        vector<int> nums(ratings.size(), 1);
        // 第一次从前向后循环,保证如果右侧评分大于左侧评分,则右侧孩子糖果数一定更多(保证了右半部分的大小关系正确)
        for(int i = 1; i < ratings.size(); i++) {
            if(ratings[i] > ratings[i-1]) nums[i] = nums[i-1] + 1;
        }
        // 第二次从后往前循环,保证如果左侧评分大于右侧评分,则左侧孩子糖果数一定更多
        for(int i = ratings.size() - 2; i > -1; i--) {
            if(ratings[i] > ratings[i+1]) {
                // nums[i] + 1 是 nums[i+1] 满足左侧条件的下限
                // nums[i+1] 是 nums[i+1] 满足右侧关系的下限
                // 两个下限必须同时满足,所以取两者最大值
                nums[i] = max(nums[i], nums[i+1] + 1);
            }  
        }
        // 计算糖果总数
        for(int i = 0; i < nums.size(); i++) {
            ans += nums[i];
        }
        return ans;
    }
};

二、加油站

题目链接: 134.加油站
题目要求:

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

提示:

gas.length == n
cost.length == n
1 <= n <= 105
0 <= gas[i], cost[i] <= 104

示例

输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

解题思路:
首先读题,根据题目和示例可以总结出以下规则:

  • 如果可以绕行一周,则一定只有一个节点可以作为出发点,需要返回该点的索引
  • 如果不能绕行一周,返回-1
  • 初始油量为0
  • 起始节点的油量一定大于等于首次前进所需的油量,是一个净增长的节点

分析题意可知,绕行过程一共有两种情况:

  1. 全程的总油量小于总需求,一定不能绕行一周
  2. 全程的油量大于总需求,但中间某一个节点处油量减小幅度最大,无法继续前行,需要更换出发节点

这个问题可以通过在for循环里嵌套while循环暴力解决。但通过应用贪心算法可以有效剪去大量无用的搜索,提升程序效率。在本题中,“贪心”贪的是油箱中的油量,每当发现当前的油量不足以前进时,我们可以直接排除从初始节点到当前节点之间的所有节点,因为初始节点是一个油量净增长的节点,如果从其他节点出发,当再次到达当前节点时,油箱中的油量一定小于本次到达该节点时油箱中的油量。因此,下一次应当从当前节点的下一个节点出发继续寻找,由于不需要重复遍历某一个节点,所以这个过程只需要一重for循环即可实现。

此时有一个问题:如果只用一重for循环遍历,当走到第n-1个节点时,是否可以从第0个节点返回起始节点?
答案由总油量和总消耗决定。
当总油量大于等于总消耗时,因为在以往的遍历过程中我们已经跳过那个最难过的节点,所以其余节点一定是可以通过的(以前油箱没油都可以过,现在油箱有油当然也可以),而因为通过绕过最难点,此时我们已经收集了足够的油来度过最难点。并且还可以知道:
从出发点到跨过最难点以后油箱中剩余的油量 >= 总油量 - 总消耗,因为我们甚至还会跳过那些会让油箱油量减少的路段
当总油量小于总消耗时,无论如何也不可能绕行一圈,所以直接排除。

代码示例:

class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int n = gas.size();
        int total = 0; // 总的加油量和总的耗油量
        int tank = 0;  // 当前的油箱剩余量
        int start = 0; // 起始加油站的下标

        for (int i = 0; i < n; i++) {
            total += gas[i] - cost[i];
            tank += gas[i] - cost[i];
            if (tank < 0) {
                // 从当前起始站点无法绕一圈
                // 清空原有的记录,从下一个节点出发继续寻找
                tank = 0;
                start = i + 1;
            }
        }

        return (total >= 0) ? start : -1;
    }
};
  • 10
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值