代码随想录刷题day34|K次反转后最大的数组和&加油站&分发糖果


day34学习内容

day34主要内容

  • K次反转后最大的数组和
  • 加油站
  • 分发糖果

声明
本文思路和文字,引用自《代码随想录》

一、K次反转后最大的数组和

1005.原题链接

1.1、思路

一共经过俩次贪心的过程

  • 第一次:把数组按绝对值排序,然后把负数取反,因为这样取反后,累加和一定是更大的
  • 第二次:如果把所有负数取反后,K次没有用完,那就把数组最后一个元素取反剩余的次数
  • 经过以上2步,得到的数组和就是最大的。

1.2、代码-正确写法

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        // 先对数组按绝对值排序
        int[] sortedArray = IntStream.of(nums)
                .boxed()
                .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
                .mapToInt(Integer::intValue)
                .toArray();

        // 第一次贪心,优先对负数取反
        for (int i = 0; i < sortedArray.length; i++) {
            if (sortedArray[i] < 0 && k > 0) {
                sortedArray[i] = -sortedArray[i];
                k--;
            }
        }

        // 所有负数都取反后,如果k次没有用完,就对数组末尾取反
        if (k % 2 == 1) {
            sortedArray[sortedArray.length - 1] = -sortedArray[sortedArray.length - 1];
        }

        return Arrays.stream(sortedArray).sum();
    }
}

1.2.1、如何理解if (k % 2 == 1) ?

在这段代码中,if (k % 2 == 1)这个条件用于判断变量k(表示还可以进行的翻转次数)是否为奇数。

在整数运算中,%是取余操作符,k % 2的结果就是k除以2后的余数。如果一个数除以2的余数是1,那么这个数一定是奇数;如果余数是0,则这个数是偶数。

  • k % 2 == 1时,意味着k是奇数。在这个问题的上下文中,如果经过前面的操作后k仍为奇数,说明你还有一次翻转机会未使用。由于前面的策略是优先将负数翻转成正数,此时数组中应该没有负数了(如果有负数,那么k应该会被用完),或者k的次数多于数组中负数的数量。因此,剩下的一次翻转操作可以应用于数组中任意一个数,但为了使总和尽可能大,这里选择翻转绝对值最小的数(由于数组已经按绝对值降序排列,最后一个元素的绝对值最小),这种情况下即数组的最后一个元素。

  • 如果k % 2 == 0(即k是偶数),则没有额外的翻转需要进行,因为两次相同的翻转会相互抵消,不会影响总和。

因此,这个条件判断是为了处理最后一个可能未使用的翻转机会,确保在所有翻转机会都用完后,能够得到最大的数组总和。

1.2.2、原始nums数组=[2,-3,-1,5,-4],那么排序后数组等于什么?

看代码,是按照绝对值排序的,所以排序后数组 = [5,-4,-3,2,-1]
第一次排序后数组的值


二、加油站

134.原题链接

2.1、思路

建议看代码随想录的文字版本。

  1. 初始化变量

    • curSum用于记录从当前加油站出发,到达下一个加油站后的净油量。
    • totalSum用于记录整个路线上的净油量总和。
    • index用于标记可能的出发加油站索引。
  2. 遍历加油站

    • 对于每个加油站,计算从该加油站出发到达下一个加油站后的净油量(即gas[i] - cost[i]),并累加到curSumtotalSum中。
    • 如果在到达某个加油站后curSum变为负数,意味着无法从当前的出发点到达这个加油站。因此,需要将出发点设为这个加油站的下一个站点(index = i + 1),并将curSum重置为0,因为我们从新的出发点开始计算。
  3. 判断是否可以绕圈一周

    • 遍历结束后,检查totalSum。如果totalSum为负数,说明整个路线的总油量不足以支撑整个旅程,因此无法完成环路,返回-1
    • 如果totalSum非负,这意味着总的油量足以支撑整个旅程。由于之前的循环已经确保了从任一点出发,只要curSum为负就更换出发点,因此最后index所标记的加油站即为可以作为起点绕圈一周的加油站索引。
  4. 总结
    这个算法的核心在于两个贪心策略的应用:

  • 局部最优解:通过在curSum变为负时立即更换出发点,保证了从每一个选定的出发点到当前点的过程中,油量都是足够的。
  • 全局最优解:通过确保totalSum非负来保证整个路线上的油量足以完成全程。

这种方法的优势在于只需遍历一次加油站数组,时间复杂度为O(n),且不需要额外的空间复杂度,非常高效。

2.2、正确写法1

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        // 从当前加油站出发,油的净油量
        int curSum = 0;
        int totalSum = 0;
        int index = 0;
        for (int i = 0; i < gas.length; i++) {
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];
            // 当前加油站出发,当前的净油量为负数,
            // 说明从当前加油站到下一个加油站的油量不够,需从当前加油站的下一站出发
            if (curSum < 0) {
                index = i + 1;
                // 卡尔题解里面是index = (i + 1) % gas.length ; 
                curSum = 0;
            }
        }
        // 总的净油量为负数,那么不可能完成
        if (totalSum < 0) {
            return -1;
        }
        return index;
    }
}

2.2.1、 如何理解上面这段代码

  1. 初始化变量

    • curSum用于记录当前从某个加油站出发后的累积净汽油量(即已经加的油减去消耗的油)。
    • totalSum用于记录所有加油站的总净汽油量。
    • index用于记录可能的出发加油站编号。
  2. 遍历加油站
    循环遍历每一个加油站,计算从该加油站出发的curSum(当前净汽油量)和所有加油站的totalSum(总净汽油量)。对每个加油站,都做以下计算:

    • 将当前加油站的汽油量gas[i]与到下一站所需的汽油量cost[i]相减,然后加到curSumtotalSum上。
    • 如果在某一站curSum变为负数,说明从上一个index出发到这个加油站的途中油量不够,无法到达当前加油站。因此,需要将index更新为下一个加油站的编号(即i+1),并将curSum重置为0,以从新的加油站重新开始计算。
  3. 判断总净汽油量
    遍历完成后,用totalSum判断整个环路是否可以绕一圈。如果totalSum小于0,说明总的汽油量不足以支撑整个旅程,返回-1。如果totalSum大于等于0,说明至少存在一条路径可以让车辆绕圈一周,这时返回记录的index,即可能的出发加油站编号。

2.2.2、如何理解index = (i + 1) % gas.length ; ?

  1. 确定下一个出发点
    当当前累积的汽油量curSum在某个点变为负值时,意味着从最近一次尝试的出发点到当前加油站的途中,汽油是不足以支持行驶到当前加油站的。因此,当前加油站不能作为出发点,需要尝试下一个加油站作为新的起点。这里的i + 1就是为了将起点设置为当前加油站的下一个加油站。

  2. 循环遍历数组
    因为问题本质上是在一个圆圈上的旅行,所以当索引到达数组的末尾时,它需要回到数组的开始,形成一个闭环。% gas.length确保了index值在超过加油站数量(也就是gas数组的长度)时能够循环回到数组的开头。例如,如果gas.length是5,当i等于4时,i + 1等于5,此时5 % 5的结果是0,即数组的第一个元素,符合循环的逻辑。

综上所述,index = (i + 1) % gas.length;这行代码的意义在于,当发现从某个加油站出发无法继续行驶时,尝试将下一个加油站作为起点,并保证这个过程可以在加油站形成的闭环上顺利进行,即使尝试的新起点是数组的第一个元素。


三、分发糖果

135.原题链接

3.1、思路

  • 举个例子看懂题目吧。

假设我们有4个孩子,他们的评分分别是:ratings = [1, 2, 2, 1]。我们的目标是给每个孩子分发糖果,条件是评分更高的孩子必须比他相邻的孩子得到更多的糖果。

第一阶段:从左向右,人话讲就是右边的小孩比左边大
  1. 初始化:每个孩子至少得到1颗糖果。所以开始时,candyVec = [1, 1, 1, 1]
  2. 从左向右遍历
    • 第一个孩子的左边没有孩子,所以他得到1颗糖果。
    • 第二个孩子(评分2)比第一个孩子(评分1)评分高,所以他得到的糖果数为第一个孩子的糖果数+1,即2颗糖果。现在candyVec = [1, 2, 1, 1]
    • 第三个孩子的评分等于第二个孩子,所以他仍然只得到1颗糖果。candyVec不变。
    • 第四个孩子的评分低于第三个孩子,所以他也只得到1颗糖果。candyVec不变。

这时,candyVec = [1, 2, 1, 1]

第二阶段:从右向左,人话讲就是左边的小孩比右边大
  1. 从右向左遍历
    • 第三个孩子比第四个孩子评分高,但按照从左向右的结果,他们的糖果数是一样的。为了满足条件,我们需要给第三个孩子更多的糖果。所以,我们将第三个孩子的糖果数更新为第四个孩子的糖果数+1,即2颗糖果。现在candyVec = [1, 2, 2, 1]
    • 第二个孩子和第一个孩子相比,不需要改变,因为他们的评分和糖果数已经符合规则。

这时,candyVec = [1, 2, 2, 1]

计算总糖果数

遍历candyVec数组,计算总糖果数:1 + 2 + 2 + 1 = 6

所以,最终答案是6颗糖果。

综上,总体思路就是先确定一边,再确定另一边。

3.2、正确写法1

class Solution {
    public int candy(int[] ratings) {
        int len = ratings.length;
        int[] candyVec = new int[len];
        candyVec[0] = 1;
        // 第一阶段,从左往后
        for (int i = 1; i < len; i++) {
            candyVec[i] = (ratings[i] > ratings[i - 1]) ? candyVec[i - 1] + 1 : 1;
        }

        // 第二阶段,从右往左
        for (int i = len - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1);
            }
        }

        // 满足条件,求和
        int ans = 0;
        for (int num : candyVec) {
            ans += num;
        }
        return ans;
    }
}

3.2.1、为什么第二阶段,i要从length-2的位置开始

因为length-1正好是最后一个元素,最后一个元素没有右孩子,不需要和任何孩子比较,所以是length-2、

3.2.2、为什么第二阶段,要从右往左遍历呢?

看图
在这里插入图片描述

3.2.3 、为什么要Math.max(candyVec[i], candyVec[i + 1] + 1);?

为什么要用max?

1、因为ratings[i] > ratings[i + 1],说明左边比右边的孩子得分多,所以左边这个孩子的candy[i] = candy[i]+1,
2、又因为第一步,按照从左到右遍历顺序,已经得到了candy[i]
3、到此时,经过【1】,说明已经满足了左边比右边大。又因为按照题意,当前孩子就要比左边(前一个)孩子的糖果多,又要比右边的(后面的)孩子糖果多,此时还剩【比左边(前一个)孩子的糖果多】没有满足,
4、如果左边的比右边大,此时,candy[i] = candy[i+1]+1;
5、所以,candy[i]要在candy[i]和candy[i+1]+1之间取最大数。也就是取第一种情况和第二种情况的最大值。

总结

1.感想

  • 第三题分发糖果好难

2.思维导图

本文思路引用自代码随想录,感谢代码随想录作者。

  • 28
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值