文章目录
Day 32
01. K 次取反后最大化的数组和(No. 1005)
1.1 题目
给你一个整数数组 nums
和一个整数 k
,按以下方法修改该数组:
- 选择某个下标
i
并将nums[i]
替换为-nums[i]
。
重复这个过程恰好 k
次。可以多次选择同一个下标 i
。
以这种方式修改数组后,返回数组 可能的最大和 。
示例 1:
输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。
示例 2:
输入:nums = [3,-1,0,2], k = 3
输出:6
解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。
示例 3:
输入:nums = [2,-3,-1,5,-4], k = 2
输出:13
解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。
提示:
1 <= nums.length <= 104
-100 <= nums[i] <= 100
1 <= k <= 104
1.2 笔记
本题中的数组会有三种构成方式:
- 全是正数
- 全是负数
- 正数和负数混合型的数组
- 而对于全是正数的数组如果又
k
次取反机会的话,最好的情况肯定是k
是一个偶数,取偶数次反的最大和就是原数组的和,但如果是奇数的话,就需要将数组进行排序来找到最小的那个数字,整个数组减去最小的那个数就是最大和。 - 如果数组全是负数的话,最好的情况肯定是尽可能利用这
k
次取反 尽可能 将所有的负数变成正数- 如果完全变成正数数组但是
k
还是没有消耗完,再次返回全是正数的情况
- 如果完全变成正数数组但是
- 如果数组是正数和负数混合的情况那和全是负数是相同的处理方式,尽可能多的将负数变为正数
这样就构成了本题的贪心策略,仔细想想没有反例。
为了确定正数和负数的界限以及最小的正数在哪里,要将数组进行排序,但是注意,如果将所有的负数都遍历完后 k
还有剩余就需要 再次排序 来获取最小的正数。
先尽可能多的将负数变为正数:
Arrays.sort(nums);
// 先尽可能将所有的负数都去除掉
for (int i = 0; i < nums.length && nums[i] < 0; i++) {
if (k == 0 || i > nums.length - 1) {
// 说明使用 k 次取反没有将所有的负数取完
return getSum(nums);
}
nums[i] = 0 - nums[i];
k--;
}
如果在这个途中使得 k
变为了负数,那此时的结果就是最大的。
遍历结束后说明是此时数组全部变为正数
if (k % 2 == 0) {
return getSum(nums);
} else {
Arrays.sort(nums);
return getSum(nums) - nums[0] - nums[0];
}
此时再去判断 k
是奇数还是偶数来进行处理。
1.3 代码
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
int res = 0;
Arrays.sort(nums);
// 先尽可能将所有的负数都去除掉
for (int i = 0; i < nums.length && nums[i] < 0; i++) {
if (k == 0 || i > nums.length - 1) {
// 说明使用 k 次取反没有将所有的负数取完
return getSum(nums);
}
nums[i] = 0 - nums[i];
k--;
}
if (k % 2 == 0) {
return getSum(nums);
} else {
Arrays.sort(nums);
return getSum(nums) - nums[0] - nums[0];
}
}
public int getSum(int[] nums) {
int res = 0;
for (int i : nums) {
res += i;
}
return res;
}
}
02. 加油站(No. 134)
2.1 题目
在一条环路上有 n
个加油站,其中第 i
个加油站有汽油 gas[i]
升。
你有一辆油箱容量无限的的汽车,从第 i
个加油站开往第 i+1
个加油站需要消耗汽油 cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas
和 cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1
。如果存在解,则 保证 它是 唯一 的。
示例 1:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
示例 2:
输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
提示:
gas.length == n
cost.length == n
1 <= n <= 105
0 <= gas[i], cost[i] <= 104
2.2 笔记
本题的解法可以说是非常的巧妙。
由题目中给出的两个数组可以推出一个新的数组 rest[gas.length]
这个数组表示从这个加油站 到达下一个加油站 剩余的油量。
如果这个数组求和得到的结果小于零,那么肯定无法环绕。
但如果这个数组求和大于等于零的话,那就一定能环绕一圈吗?
答案是一定可以,搞清楚这个就是解题的关键了。
肯定可以将整段路程(rest
数组)分为两个阶段:
首先来确定这两个阶段的的和,答案肯定是正数或者零,那假如题目中给的恰好是 0
,那第一段的和就恰好是第二段的 相反数,也就是如果 通过第二段 到达终点,此时的油量恰好能支持他走回来。
也就是说此时的题目就变成了寻找从哪个节点开始 走到终点 油量为正数。
首先构建出 rest
数组
int[] rest = new int[gas.length];
int sum = 0;
for(int i = 0; i < gas.length; i++) {
sum += gas[i] - cost[i];
rest[i] = gas[i] - cost[i];
}
然后来判断这个数组的总和是否大于等于 0
if (sum < 0) {
return -1;
}
然后去寻找这个节点,从节点 0 开始寻找,如果途中发现 sum
变为负数就从 i + 1
继续寻找,直到遍历完成一定能找到一个节点。
for (int i = 0; i < rest.length; i++) {
currentSum += rest[i];
if (currentSum < 0) {
currentSum = 0;
startIndex = i + 1;
}
}
此时的节点就是结果。
简化一下,写出代码。
2.3 代码
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int sum = 0; // rest 数组的总数
int currentSum = 0;
int startIndex = 0;
for(int i = 0; i < gas.length; i++) {
currentSum += gas[i] - cost[i];
if (currentSum < 0) {
currentSum = 0;
startIndex = i + 1;
}
}
if (sum < 0) {
return -1;
}
return startIndex;
}
}
03. 分发糖果(No. 135)
3.1 题目
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。 - 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
提示:
n == ratings.length
1 <= n <= 2 * 104
0 <= ratings[i] <= 2 * 104
3.2 笔记
本题难的地方就在于一个节点的变化可能会导致整个分发情况的变化。
所以本题一定要 整体 来求解,即一次只考虑一种情况:左边大于右边或者右边大于左边
这样遍历一个整体就能保证最后的结果满足其中的一个,通过两次对整体的修改就能得到最终的结果。
先从头开始遍历处理,这里必须要注意遍历顺序的有效性,因为后面是要 在前面的基础上 去做一个自增操作,所以从前往后遍历只能是解决 右边比左边大 的情况,这样才能保证操作是准确的。
从前向后遍历不断更新,可以得到如下的数组
// 先从前向后去遍历,右边比左边大的情况
for (int i = 1; i < ratings.length; i++) {
nums[i] = 1;
if (ratings[i] > ratings[i - 1]) {
nums[i] = nums[i - 1] + 1;
}
}
这就保证右边比左边大的情况全部处理了,现在还需要处理左边比右边大的情况,此时为了结果的有效就需要从后往前去遍历了。
注意上图中的 2 + 1
中的 2
是后一个节点的 2
而不是第一次遍历得到的 2
// 再从后往前去遍历,左边比右边大的情况
for (int i = ratings.length - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
nums[i] = Math.max(nums[i], nums[i + 1] + 1);
}
}
第二次遍历就会出现某个节点的糖果数量的改变,一旦改变就说明这个节点 大于左边的节点,同时大于右边的节点,那此时这个节点的值就应该是其中的最大值
nums[i] = Math.max(nums[i], nums[i + 1] + 1);
通过这种前后遍历来分别处理 改变一个节点的数值造成的影响 就能保证不会出现纰漏
3.3 代码
class Solution {
public int candy(int[] ratings) {
int[] nums = new int[ratings.length];
Arrays.fill(nums, 1);
// 先从前向后去遍历,右边比左边大的情况
for (int i = 1; i < ratings.length; i++) {
nums[i] = 1;
if (ratings[i] > ratings[i - 1]) {
nums[i] = nums[i - 1] + 1;
}
}
// 再从后往前去遍历,左边比右边大的情况
for (int i = ratings.length - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
nums[i] = Math.max(nums[i], nums[i + 1] + 1);
}
}
return Arrays.stream(nums).sum();
}
}