文章目录
- day34学习内容
- 一、K次反转后最大的数组和
- 1.1、思路
- 1.2、代码-正确写法
- 1.2.1、如何理解if (k % 2 == 1) ?
- 1.2.2、原始nums数组=[2,-3,-1,5,-4],那么排序后数组等于什么?
- 二、加油站
- 2.1、思路
- 2.2、正确写法1
- 2.2.1、 如何理解上面这段代码
- 2.2.2、如何理解index = (i + 1) % gas.length ; ?
- 三、分发糖果
- 3.1、思路
- 第一阶段:从左向右,人话讲就是右边的小孩比左边大
- 第二阶段:从右向左,人话讲就是左边的小孩比右边大
- 计算总糖果数
- 3.2、正确写法1
- 3.2.1、为什么第二阶段,i要从length-2的位置开始
- 3.2.2、为什么第二阶段,要从右往左遍历呢?
- 3.2.3 、为什么要Math.max(candyVec[i], candyVec[i + 1] + 1);?
- 总结
- 1.感想
- 2.思维导图
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、思路
建议看代码随想录的文字版本。
-
初始化变量:
curSum
用于记录从当前加油站出发,到达下一个加油站后的净油量。totalSum
用于记录整个路线上的净油量总和。index
用于标记可能的出发加油站索引。
-
遍历加油站:
- 对于每个加油站,计算从该加油站出发到达下一个加油站后的净油量(即
gas[i] - cost[i]
),并累加到curSum
和totalSum
中。 - 如果在到达某个加油站后
curSum
变为负数,意味着无法从当前的出发点到达这个加油站。因此,需要将出发点设为这个加油站的下一个站点(index = i + 1
),并将curSum
重置为0,因为我们从新的出发点开始计算。
- 对于每个加油站,计算从该加油站出发到达下一个加油站后的净油量(即
-
判断是否可以绕圈一周:
- 遍历结束后,检查
totalSum
。如果totalSum
为负数,说明整个路线的总油量不足以支撑整个旅程,因此无法完成环路,返回-1
。 - 如果
totalSum
非负,这意味着总的油量足以支撑整个旅程。由于之前的循环已经确保了从任一点出发,只要curSum
为负就更换出发点,因此最后index
所标记的加油站即为可以作为起点绕圈一周的加油站索引。
- 遍历结束后,检查
-
总结:
这个算法的核心在于两个贪心策略的应用:
- 局部最优解:通过在
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、 如何理解上面这段代码
-
初始化变量:
curSum
用于记录当前从某个加油站出发后的累积净汽油量(即已经加的油减去消耗的油)。totalSum
用于记录所有加油站的总净汽油量。index
用于记录可能的出发加油站编号。
-
遍历加油站:
循环遍历每一个加油站,计算从该加油站出发的curSum
(当前净汽油量)和所有加油站的totalSum
(总净汽油量)。对每个加油站,都做以下计算:- 将当前加油站的汽油量
gas[i]
与到下一站所需的汽油量cost[i]
相减,然后加到curSum
和totalSum
上。 - 如果在某一站
curSum
变为负数,说明从上一个index
出发到这个加油站的途中油量不够,无法到达当前加油站。因此,需要将index
更新为下一个加油站的编号(即i+1
),并将curSum
重置为0,以从新的加油站重新开始计算。
- 将当前加油站的汽油量
-
判断总净汽油量:
遍历完成后,用totalSum
判断整个环路是否可以绕一圈。如果totalSum
小于0,说明总的汽油量不足以支撑整个旅程,返回-1
。如果totalSum
大于等于0,说明至少存在一条路径可以让车辆绕圈一周,这时返回记录的index
,即可能的出发加油站编号。
2.2.2、如何理解index = (i + 1) % gas.length ; ?
-
确定下一个出发点:
当当前累积的汽油量curSum
在某个点变为负值时,意味着从最近一次尝试的出发点到当前加油站的途中,汽油是不足以支持行驶到当前加油站的。因此,当前加油站不能作为出发点,需要尝试下一个加油站作为新的起点。这里的i + 1
就是为了将起点设置为当前加油站的下一个加油站。 -
循环遍历数组:
因为问题本质上是在一个圆圈上的旅行,所以当索引到达数组的末尾时,它需要回到数组的开始,形成一个闭环。% 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颗糖果。所以开始时,
candyVec = [1, 1, 1, 1]
。 - 从左向右遍历:
- 第一个孩子的左边没有孩子,所以他得到1颗糖果。
- 第二个孩子(评分2)比第一个孩子(评分1)评分高,所以他得到的糖果数为第一个孩子的糖果数+1,即2颗糖果。现在
candyVec = [1, 2, 1, 1]
。 - 第三个孩子的评分等于第二个孩子,所以他仍然只得到1颗糖果。
candyVec
不变。 - 第四个孩子的评分低于第三个孩子,所以他也只得到1颗糖果。
candyVec
不变。
这时,candyVec = [1, 2, 1, 1]
。
第二阶段:从右向左,人话讲就是左边的小孩比右边大
- 从右向左遍历:
- 第三个孩子比第四个孩子评分高,但按照从左向右的结果,他们的糖果数是一样的。为了满足条件,我们需要给第三个孩子更多的糖果。所以,我们将第三个孩子的糖果数更新为第四个孩子的糖果数+1,即2颗糖果。现在
candyVec = [1, 2, 2, 1]
。 - 第二个孩子和第一个孩子相比,不需要改变,因为他们的评分和糖果数已经符合规则。
- 第三个孩子比第四个孩子评分高,但按照从左向右的结果,他们的糖果数是一样的。为了满足条件,我们需要给第三个孩子更多的糖果。所以,我们将第三个孩子的糖果数更新为第四个孩子的糖果数+1,即2颗糖果。现在
这时,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.思维导图
本文思路引用自代码随想录,感谢代码随想录作者。