代码随想录算法训练营第三十四天_第八章_贪心算法 | 1005.K次取反后最大化的数组和、134. 加油站、135. 分发糖果

        给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)

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

视频讲解icon-default.png?t=N0U7https://www.bilibili.com/video/BV138411G7LY/?spm_id_from=333.788&vd_source=f98f2942b3c4cafea8907a325fc56a48文章讲解https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%90%8E%E6%9C%80%E5%A4%A7%E5%8C%96%E7%9A%84%E6%95%B0%E7%BB%84%E5%92%8C.html

  • 思路:两次贪心
  1. 绝对值大的负数变为正数(局部最优)👉整个数组和达到最大(全局最优);
  2. 如果将负数都转变为正数了,K依然大于0,局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大👉全局最优:整个数组和达到最大。
  • 具体步骤:
    • 第一步:将数组按照绝对值大小从大到小排序注意要按照绝对值的大小
    • 第二步:从前向后遍历,遇到负数将其变为正数,同时K--
    • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
    • 第四步:求和
  • 代码:
class Solution {
static bool cmp(int a, int b) {
    return abs(a) > abs(b);
}
public:
    int largestSumAfterKNegations(vector<int>& A, int K) {
        sort(A.begin(), A.end(), cmp);       // 第一步
        for (int i = 0; i < A.size(); i++) { // 第二步
            if (A[i] < 0 && K > 0) {
                A[i] *= -1;
                K--;
            }
        }
        if (K % 2 == 1) A[A.size() - 1] *= -1; // 第三步
        int result = 0;
        for (int a : A) result += a;        // 第四步
        return result;
    }
};

LeetCode 134. 加油站

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

        如果你可以绕环路行驶一周,则返回 出发时加油站的编号,否则返回 -1。

  • 如果题目有解,该答案即为唯一答案。
  • 输入数组均为非空数组,且 gas 与 cost 长度相同。
  • 输入数组中的元素均为非负数。

视频讲解icon-default.png?t=N0U7https://www.bilibili.com/video/BV1jA411r7WX/?spm_id_from=333.788&vd_source=f98f2942b3c4cafea8907a325fc56a48

文章讲解https://programmercarl.com/0134.%E5%8A%A0%E6%B2%B9%E7%AB%99.html

  • 思路:
    • 暴力解法:
      • 遍历每一个加油站为起点的情况,模拟一圈。
      • 如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。
      • for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历!
    • 贪心算法:
      • 方法一:

        情况一:

               如果 gas的总和 小于 cost总和,那么无论从哪里出发,都跑不了一圈;

        情况二:

                rest[i] = gas[i] - cost[i] 为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。

        情况三:

                如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能将这个负数填平,即为出发节点。 

      • ⭐方法二:

        每个加油站的剩余量rest[i]为gas[i] - cost[i]。

        i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。

  • 代码:
// 暴力解法
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        for (int i = 0; i < cost.size(); i++) {
            int rest = gas[i] - cost[i]; // 记录剩余油量
            int index = (i + 1) % cost.size();
            while (rest > 0 && index != i) { // 模拟以i为起点行驶一圈
                rest += gas[index] - cost[index];
                index = (index + 1) % cost.size();
            }
            // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
            if (rest >= 0 && index == i) return i;
        }
        return -1;
    }
};
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
// 贪心算法:方法一
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curSum = 0;
        int min = INT_MAX; // 从起点出发,油箱里的油量最小值
        for (int i = 0; i < gas.size(); i++) {
            int rest = gas[i] - cost[i];
            curSum += rest;
            if (curSum < min) {
                min = curSum;
            }
        }
        if (curSum < 0) return -1;  // 情况1
        if (min >= 0) return 0;     // 情况2
                                    // 情况3
        for (int i = gas.size() - 1; i >= 0; i--) {
            int rest = gas[i] - cost[i];
            min += rest;
            if (min >= 0) {
                return i;
            }
        }
        return -1;
    }
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)
// 贪心算法:方法二
class Solution {
public:
    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curSum = 0;
        int totalSum = 0;
        int start = 0;
        for (int i = 0; i < gas.size(); i++) {
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];
            if (curSum < 0) {   // 当前累加rest[i]和 curSum一旦小于0
                start = i + 1;  // 起始位置更新为i+1
                curSum = 0;     // curSum从0开始
            }
        }
        if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了
        return start;
    }
};
// 时间复杂度:O(n)
// 空间复杂度:O(1)

LeetCode 135. 分发糖果

        老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

老师至少需要准备多少颗糖果呢?

  • 输入: [1,2,2]
  • 输出: 4
  • 解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果,第三个孩子只得到 1 颗糖果

视频讲解icon-default.png?t=N0U7https://www.bilibili.com/video/BV1ev4y1r7wN/?spm_id_from=333.788&vd_source=f98f2942b3c4cafea8907a325fc56a48文章讲解icon-default.png?t=N0U7https://programmercarl.com/0135.%E5%88%86%E5%8F%91%E7%B3%96%E6%9E%9C.html

  • 思路:确定一边之后,再确定另一边(先比较每一个孩子 i 的左边 i - 1,再比较右边 i + 1)
    • 从前向后遍历
      // 从前向后
      for (int i = 1; i < ratings.size(); i++) {
          if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1;
      }
      • 局部最优:只要右边评分比左边大,右边的孩子就多一个糖果
      • 全局最优:相邻的孩子中,评分高的右边孩子获得比左边孩子更多的糖果
    • 从后向前遍历
      // 从后向前
      for (int i = ratings.size() - 2; i >= 0; i--) {
          if (ratings[i] > ratings[i + 1] ) {
              candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
          }
      }

      如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,又比右边candyVec[i + 1]的糖果多

  • 代码:
class Solution {
public:
    int candy(vector<int>& ratings) {
        vector<int> candyVec(ratings.size(), 1);
        // 从前向后
        for (int i = 1; i < ratings.size(); i++) {
            if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1;
        }
        // 从后向前
        for (int i = ratings.size() - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1] ) {
                candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1);
            }
        }
        // 统计结果
        int result = 0;
        for (int i = 0; i < candyVec.size(); i++) result += candyVec[i];
        return result;
    }
};

总结:如果在考虑局部的时候想两边兼顾,就会顾此失彼。

采用了两次贪心的策略:

  • 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
  • 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值