【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;
}

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

<think>好的,我现在需要决用户关于蓝桥杯“分糖果”问题的法。用户已经提供了几个相关的引用,我需要先仔细阅读这些引用,理各个题目的内容和提供的法,然后整理出针对“分糖果”问题的有效决方案。 首先,用户提到的引用中有两个不同的分糖果问题。引用2和引用5讨论的是一个分糖果的循环过程,小朋友分一半糖果给左手边的孩子,然后老师补发奇数颗糖的孩子。而引用1、3、4似乎涉及不同的题目,比如拿糖果的动态规划和贪心算法。用户的问题可能集中在引用2和5提到的分糖果问题上,因此我需要重点关注这两个引用。 接下来,引用2中的问题描述是:每个小朋友将糖果分一半给左手边的孩子,分完后如果有奇数颗糖,老师补发1颗,直到所有孩子糖果数相同。需要计算老师补发的总糖果数。引用5提到使用两个数组来处理分糖过程,其中一个保存上一次的状态,以避免在更新过程中覆盖当前值。这可能涉及到模拟每一轮分糖的过程,直到所有糖果数相等。 我需要先明确问题的具体步骤。首先,每一轮分糖分为两个步骤:分糖和补发。分糖时,每个孩子将当前糖果的一半分给左边,但这里可能存在顺序问题,因为如果直接修改原数组,后面的孩子分糖时会使用已更新的值,导致错误。因此,需要同时保存当前和上一轮的状态,或者使用临时数组来处理分糖过程。 例如,假设有孩子A、B、C,初始糖果数为3, 5, 7。分糖时,每个孩子分出一半。如果直接处理,可能后面的孩子分糖时前面的已经改变了。因此,正确的做法是先将所有孩子的一半糖果计算出来,然后统一分配。或者使用两个数组,一个保存当前状态,另一个用来计算下一轮的状态。 引用5中提到使用两个数组,kfront数组保存上一次分糖后的结果,这样在分糖时,每个孩子的新糖果数是kfront[i]的一半加上kfront左边孩子的一半。这可能是因为分糖是同时进行的,所以需要用到原来的值进行计算,而不是实时更新的值。 接下来,我需要模拟这个过程。例如,初始数组为kfront,然后计算每个孩子的新糖果数为kfront[i]//2 + kfront[i-1]//2(注意第一个孩子的左边是最后一个孩子),然后处理奇数的情况,老师补发。这个过程需要循环直到所有孩子糖果数相同。 此外,需要注意在分糖过程中,每个孩子先分出自己原来的一半,所以需要先将所有孩子的分出一半后的数值计算出来,再分配。否则,如果直接修改原数组,后面的孩子分糖时会使用已更新的值,导致错误。 因此,正确的步骤应该是: 1. 初始化一个数组,保存当前每个孩子糖果数。 2. 循环直到所有数相同: a. 创建一个临时数组,保存分糖后的结果。 b. 对于每个孩子i,计算临时数组[i] = 原数组[i] // 2 + 原数组[i-1] // 2(处理第一个孩子的左边是最后一个)。 c. 检查每个临时数组中的数值是否为奇数,如果是,补发1颗,并统计补发总数。 d. 将原数组更新为临时数组。 3. 当所有孩子糖果数相同时,结束循环,返回补发总数。 在这个过程中,必须确保分糖时使用的是原数组的值,而不是在分的过程中被修改的值,因此使用临时数组是正确的做法。同时,每次分糖后需要检查奇数并补发,这可能会影响下一轮的分糖过程。 现在,我需要考虑如何用代码实现这个逻辑。例如,使用Python的话,可以用一个循环,每次迭代处理分糖、补发,并检查是否所有元素相同。 可能的代码结构如下: 初始化糖果数组candies,补发总数count=0 循环: 创建临时数组temp,每个元素为当前元素//2 + 前一个元素//2(注意第一个的前一个是最后一个) 检查每个temp元素是否为奇数,如果是,加1,count增加 检查temp中所有元素是否相等,如果是,退出循环 否则,将candies更新为temp 返回count 但这里需要注意,原数组的更新应该是在所有分糖操作之后,所以必须用临时数组保存所有新值后再覆盖原数组。 例如,对于每个孩子i的新糖果数,是当前i和i-1的原来值的一半相加。因此,在分糖前,必须保存所有原值,或者使用一个临时数组来保存中间结果。或者,在分糖时,每个孩子分出一半,但可能需要先计算所有孩子分出去的一半,然后统一分配。但根据问题描述,每个孩子分一半给左边,因此每个孩子i的左边是i-1,而每个孩子i的新糖果数应该是自己原来的一半(分出去后剩下的)加上右边孩子分给他的一半。或者可能我理错了分糖的方向? 根据引用2的描述:“每个小朋友都把自己的糖果分一半给左手边的孩子。”,所以每个孩子i会分出一半给左边的孩子i-1。例如,孩子i原来的糖果是a,分出一半即a//2给左边,剩下的a - a//2。同时,右边的孩子i+1也会分出一半给i。因此,孩子i的新糖果数应该是自己剩下的部分(原a - a//2)加上右边孩子分给他的一半(即原i+1的糖果数//2)。 但这里需要注意循环的结构,比如第一个孩子的左边是最后一个孩子。例如,三个孩子0,1,2,孩子0的左边是孩子2,孩子1的左边是0,孩子2的左边是1。 因此,正确的分糖过程应该是: 对于每个孩子i的新糖果数,等于他自己剩下的(原i的糖果- 原i的糖果数//2)加上右边孩子分给他的(原i+1的糖果数//2)。或者,可能需要重新考虑分糖的方向。 或者,可能每个孩子i将一半分给左边,即左边的是i-1,所以i的新糖果数等于他自己剩下的(原i - 原i//2)加上左边传来的分糖(原i-1的//2)? 这需要明确分糖的方向。比如,每个孩子i分给左边(i-1),那么当分糖时,每个孩子i都会给i-1自己的糖果数的一半。此时,每个孩子i得到的是来自i+1的糖果的一半吗?或者,每个孩子i得到的是左边孩子i-1分来的?或者这里可能我混淆了方向。 例如,孩子i分给左边i-1,那么i-1得到的是i的一半。同时,孩子i-1自己分给i-2,那么i得到的是i+1的一半? 这个逻辑可能需要更仔细的分析。例如,假设孩子们一个圈,每个孩子同时分一半给左边。那么,每个孩子i在分糖之后的新糖果数应该是他自己原来的糖果数减去他分出去的一半(即原i//2)加上他右边孩子分给他的一半(原i+1的糖果数//2)。 例如,孩子i分给i-1,所以i分出去原i//2,自己剩下原i - 原i//2。而同时,孩子i+1分给i,因此i得到原i+1//2。因此,新的糖果数是(原i - 原i//2) + (原i+1//2)。 这样,每个孩子i的新糖果数取决于原i和原i+1的值。因此,在分糖时,必须使用原数组的值,不能实时更新,否则会导致顺序问题。因此,必须使用临时数组来保存新的糖果数,这样每个i的新值都是基于原数组的值计算的。 因此,正确的分糖步骤应该是: 创建一个临时数组temp,长度与candies相同。 对于每个i从0到n-1: temp[i] = (candies[i] - candies[i] // 2) + (candies[(i+1) % n] // 2) 这里,candies[i]//2是分给左边i-1的,而自己剩下的部分是candies[i] - candies[i]//2。然后,i得到的是右边i+1的糖果数//2,因为i+1分给i的是自己的糖果数的一半,即candies[(i+1)%n]//2。所以,i的新糖果数等于自己剩下的部分加上右边分来的部分。 例如,假设孩子i原来的糖果数是a,那么分出去a//2给左边,自己剩下a - a//2。同时,右边孩子i+1原来的糖果数是b,分给i的是b//2。因此,i的新糖果数是 (a - a//2) + b//2。 这样,temp[i]的计算是正确的。 因此,分糖的正确步骤是:对于每个孩子i,计算temp[i] = (candies[i] - candies[i] // 2) + candies[next_i] // 2,其中next_i是右边的孩子,即(i+1) % n。 这样,在分糖时,每个孩子的新糖果数是基于原数组中的值计算的,因此必须使用临时数组保存所有新值,然后再替换原数组。 然后,分糖完后,检查每个temp中的元素是否为奇数,如果是,补发1颗,并统计补发次数。例如,对于每个i,如果temp[i] % 2 != 0,则temp[i] +=1,count +=1。 之后,检查temp中的所有元素是否相同。如果是,结束循环;否则,将candies数组更新为temp,继续下一轮。 因此,代码的大致流程如下: n = len(candies) count = 0 while True: # 创建临时数组 temp = [] for i in range(n): give = candies[i] // 2 right = (i +1) %n receive = candies[right] // 2 new = (candies[i] - give) + receive temp.append(new) # 检查是否需要补发 for i in range(n): if temp[i] % 2 !=0: temp[i] +=1 count +=1 # 检查是否全部相同 if all(x == temp[0] for x in temp): break # 更新candies candies = temp.copy() return count 注意,在Python中,数组的赋值需要深拷贝,否则可能会有引用问题。例如,candies = temp.copy() 或者使用列表生式。 现在,我需要验证这个逻辑是否正确。例如,假设初始糖果数为3,5,7: 第一轮分糖: 每个孩子i的分糖: i=0: candies[0] =3 → give=1,自己剩下2。右边是i=1,candies[1]=5→ receive=2(5//2=2)。所以temp[0] =2+2=4. i=1: candies[1]=5 → give=2,自己剩下3。右边是i=2,candies[2]=7 → receive=3(7//2=3). temp[1] =3+3=6. i=2: candies[2]=7 → give=3,自己剩下4。右边是i=0,candies[0]=3 → receive=1(3//2=1). temp[2]=4+1=5. 现在temp数组是[4,6,5]。检查奇数: 4是偶,6偶,5奇。所以补发1,变5+1=6。此时temp变为[4,6,6],补发次数为1。 此时数组变为[4,6,6],但并非全部相同,继续下一轮。 第二轮分糖: candies数组是[4,6,6] i=0: give=4//2=2 → 剩下2。右边i=1,6→ receive 3. temp[0]=2+3=5. i=1: give=6//2=3 → 剩下3. 右边i=2,6→ receive 3. temp[1]=3+3=6. i=2: give=6//2=3 → 剩下3. 右边i=0,4→ receive 2. temp[2]=3+2=5. temp数组是[5,6,5]. 检查奇数: 5奇 → 补发1,变6。补发次数增加2(i=0和i=2各补发一次)。此时temp是[6,6,6]. 补发次数累计1+2=3. 此时所有元素相同,循环结束。总补发次数为3,正确吗? 原问题可能给出的例子需要确认。例如,如果初始输入是3孩子糖果数为3、5、7,那么老师需要补发3次,最终每个孩子有6颗。这可能正确。 因此,上述逻辑是正确的。现在,我需要将这个逻辑转化为代码,并确保正确。 此外,引用5中提到使用两个数组来处理分糖过程,这可能与上述临时数组的方法是一致的。例如,kfront数组保存上一次的状态,用于计算当前的分配。 综上所述,分糖果问题的法是模拟每一轮的分糖和补发过程,直到所有孩子糖果数相同,统计补发次数。需要注意使用临时数组来确保分糖的正确性。</think>### 蓝桥杯“分糖果”问题法分析 题目要求:$N$个小朋友围坐分糖果,每轮操作中每个小朋友将一半糖果分给左边的小朋友。分完后若某个小朋友糖果数为奇数,老师补发1颗。求所有小朋友糖果数相同时老师补发的总次数[^2][^5]。 #### 算法思路 1. **模拟分糖过程**:使用临时数组保存每轮调整后的状态,确保计算时使用原始数据 2. **奇偶校验**:每次分糖后遍历检查奇数并补发 3. **终止条件**:检测所有元素是否相等时跳出循环 $$时间复杂度:O(k \cdot N),其中k为调整轮数$$ #### Python代码实现 ```python def distribute_candies(candies): n = len(candies) count = 0 while True: temp = [] # 分糖操作 for i in range(n): right = (i + 1) % n give = candies[i] // 2 receive = candies[right] // 2 new = (candies[i] - give) + receive temp.append(new) # 补发奇数糖果 for i in range(n): if temp[i] % 2 != 0: temp[i] += 1 count += 1 # 检查终止条件 if all(x == temp[0] for x in temp): break candies = temp.copy() return count # 示例输入 print(distribute_candies([3,5,7])) # 输出:3 ``` #### 关键步骤说明 1. **分糖方向处理**:每个小朋友的右边索引通过$(i+1)\%n$实现环形结构 2. **数值更新策略**:分糖时保留原始值的$candies[i]//2$,避免实时修改导致数据污染 3. **空间优化**:使用临时数组存储中间状态,空间复杂度保持$O(N)$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

攻城狮7号

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

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

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

打赏作者

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

抵扣说明:

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

余额充值