【3.3】贪心算法-解分发糖果

一、题目

        老师想给孩子们分发糖果,有N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
        你需要按照以下要求,帮助老师给这些孩子分发糖果:
        1)每个孩子至少分配到1 个糖果。
        2)评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
        那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:
输入:[ 1 , 0 , 2 ]
输出:5
解释:你可以分别给这三个孩子分发2、1、2颗糖果。

示例 2:
输入:[ 1 , 2 , 2 ]
输出:4
解释:你可以分别给这三个孩子分发1、2、1颗糖果。第三个孩子只得到1颗糖果,这已满足上述两个条件。

二、解题思路

这个问题涉及三个关键条件:
1. 每个孩子至少获得一个糖果。
2. 得分高于相邻孩子的孩子应得到更多的糖果。
3. 计算出满足上述条件所需的最少糖果总数。

解决步骤如下:
首先,为了同时满足条件1和条件3,我们可以简单地给每个孩子分配一个糖果。
其次,为了满足条件2和条件3,我们需要进一步调整糖果的分配:
        1)如果一个孩子的得分仅高于其左侧的孩子,那么他应比左侧的孩子多获得一个糖果。
        2)如果一个孩子的得分仅高于其右侧的孩子,那么他应比右侧的孩子多获得一个糖果。
        3)如果一个孩子的得分同时高于其左右两侧的孩子,那么他应比左右两侧孩子中获得糖果较多的那个孩子再多获得一个糖果。

为了实现上述调整,我们可以采用两次遍历的方法:
        第一次遍历从左到右进行,确保每个得分高于其左侧邻居的孩子获得比左侧孩子多一个糖果。这样处理后,每个孩子都满足了其右侧邻居得分高于自己时的情况。
        第二次遍历从右到左进行,确保每个得分高于其右侧邻居的孩子获得比右侧孩子多一个糖果。通过这次遍历,我们进一步满足了每个孩子左侧邻居得分高于自己时的情况。

        通过这两次遍历,我们可以确保每个孩子都根据其得分获得了正确的糖果数量,从而满足了所有条件并计算出了所需的最少糖果总数。随便举个例子画个图来看一下

三、代码实现

原理简单,来看看代码:

#include <iostream>
#include <vector>
#include <algorithm>

int candy(std::vector<int>& ratings) {
    int length = ratings.size();
    // 记录的是从左往右循环的结果
    std::vector<int> left(length, 1);
    // 记录的是从右往左循环的结果
    std::vector<int> right(length, 1);

    // 先从左往右遍历,如果当前孩子的得分比左边的高,
    // 那么当前孩子的糖果要比左边孩子的多一个
    for (int i = 1; i < length; i++) {
        if (ratings[i] > ratings[i - 1]) {
            left[i] = left[i - 1] + 1;
        }
    }

    // 然后再从右往左遍历,如果当前孩子的得分比右边的高,
    // 那么当前孩子的糖果要比右边边孩子的多一个
    for (int i = length - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1]) {
            right[i] = right[i + 1] + 1;
        }
    }

    // 要满足左右两边的条件,那么当前孩子的糖果就要取
    // 从左往右和从右往左的最大值。
    int total = 0;
    for (int i = 0; i < length; i++) {
        // 累计每个孩子的糖果
        total += std::max(left[i], right[i]);
    }

    return total;
}

int main() {
    std::vector<int> ratings = {1, 0, 2};
    int result = candy(ratings);
    std::cout << "Minimum number of candies: " << result << std::endl;
    return 0;
}
        其实还可以优化一下,在从右往左遍历的时候就可以统计total 的值了,不需要再使用第 3个 for 循环,代码如下
#include <iostream>
#include <vector>
#include <algorithm>

int candy(std::vector<int>& ratings) {
    int length = ratings.size();
    std::vector<int> count(length, 1); // 默认给每个孩子一个糖果

    // 先从左往右遍历
    for (int i = 1; i < length; i++) {
        if (ratings[i] > ratings[i - 1]) {
            count[i] = count[i - 1] + 1;
        }
    }

    // 从右往左遍历,然后顺便累加total的值
    int total = count[length - 1]; // total的默认值就是最后那个孩子的糖果数量
    for (int i = length - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1]) {
            count[i] = std::max(count[i], count[i + 1] + 1);
        }
        total += count[i];
    }

    return total;
}

int main() {
    std::vector<int> ratings = {1, 0, 2};
    int result = candy(ratings);
    std::cout << "Minimum number of candies: " << result << std::endl;
    return 0;
}

        我们也可以通过一次从左到右的遍历来解决这个问题。在遍历过程中,我们需要考虑三种情况来确定当前孩子的糖果数量:

1. **递增情况**:如果当前孩子的评分比左边的孩子高,那么当前孩子的糖果数量应该比左边的孩子多一个。
2. **相等情况**:如果当前孩子的评分与左边的孩子相同,我们暂时将当前孩子的糖果数量设为1,但这个值可能会在后续的遍历中调整。
3. **递减情况**:如果当前孩子的评分比左边的孩子低,我们无法立即确定当前孩子的糖果数量。不过,我们可以统计递减序列的长度,因为递减序列的最后一个孩子的糖果数量必定是1,并且从后往前糖果数量逐渐增加。

为了更好地理解,我们以数组 `[1, 3, 4, 6, 8, 5, 3, 1]` 为例:

- 从左到右,评分递增的部分是 `[1, 3, 4, 6, 8]`,对应的糖果数量是 `[1, 2, 3, 4, 5]`。
- 评分递减的部分是 `[8, 5, 3, 1]`,对应的糖果数量是 `[5, 3, 2, 1]`。

最高点8的糖果数量是左右两边的最大值加1,即 `5 + 1 = 6`。

计算总糖果数量时,递增部分的糖果数量是 `(1 + 5) * 5 / 2 = 15`,递减部分的糖果数量是 `(1 + 3) * 3 / 2 = 6`,加上最高点8的糖果数量,总糖果数量是 `15 + 6 + 6 = 27`。

通过这种方式,我们可以有效地计算出满足所有条件的总糖果数量。

上面的数组的单调性用图像来表示就是

但实际上数组不一定都是这样的,有可能是这样的,比如 [2,5,7,4,3,1,5,7,8,6,4,3]

        这种情况下,我们可以将数组拆分为两个子数组 `[2, 5, 7, 4, 3, 1]` 和 `[1, 5, 7, 8, 6, 4, 3]` 来分别进行计算。需要注意的是,元素 `1` 被包含在了两个子数组中,这意味着在计算总糖果数量时,元素 `1` 会被重复计算两次。因此,在最终求和时,我们需要减去一次元素 `1` 的糖果数量,以确保总糖果数量是正确的。 我们来看下代码
#include <iostream>
#include <vector>

int candy(std::vector<int>& ratings) {
    int length = ratings.size();
    if (length == 1)
        return 1;

    int index = 0;
    int total = 0;

    while (index < length - 1) {
        int left = 0;
        // 统计递增的长度
        while (index < length - 1 && ratings[index + 1] > ratings[index]) {
            index++;
            left++;
        }

        int right = 0;
        // 统计递减的长度
        while (index < length - 1 && ratings[index + 1] < ratings[index]) {
            index++;
            right++;
        }

        // 记录顶点的值,也就是左右两边最大的值加1
        int peekCount = std::max(left, right) + 1;

        // 注意这里如果total不等于0要减1,是因为把数组拆分成子数组的时候,低谷的那个会被拆到两个数组中,
        // 如果total不等于0,说明之前已经统计过,而下面会再次统计,所以要提前减去。
        if (total != 0)
            total--;

        // 当前这个子数组的糖果数量就是前面递增的加上后面递减的然后在加上顶点的。
        total += (1 + left) * left / 2 + (1 + right) * right / 2 + peekCount;

        // 如果当前孩子的得分和前一个一样,我们让他降为1
        while (index < length - 1 && ratings[index + 1] == ratings[index]) {
            index++;
            total++;
        }
    }

    return total;
}

int main() {
    std::vector<int> ratings = {2, 5, 7, 4, 3, 1, 5, 7, 8, 6, 4, 3};
    int result = candy(ratings);
    std::cout << "Total candies: " << result << std::endl;
    return 0;
}

        贪心算法与动态规划不同之处在于,贪心算法在每一步都选择当前看起来最优的解决方案,而不考虑全局情况。对于这个问题,我们可以采用贪心策略:如果一个孩子的得分比他右边的孩子高,那么他应该得到比右边孩子多一个糖果;如果得分不比右边孩子高,那么他至少应该得到一个糖果。这样处理后,我们还需要从左到右再检查一遍,确保每个孩子的得分比他左边的孩子高时,他得到的糖果也比左边孩子多。通过这种局部最优的选择,我们可以确保满足题目要求的同时,使用的糖果数量最少。

  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城狮7号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值