@TOC
前言
代码随想录算法训练营day34
一、Leetcode 1005.K次取反后最大化的数组和
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] 。
来源:力扣(LeetCode) 链接:leetcode.cn/problems/ma…
2.解题思路
方法一:从小到大修改每个负数
思路与算法
由于我们希望数组的和尽可能大,因此除非万不得已,我们应当总是修改负数,并且优先修改值最小的负数。因为将负数 −x−x 修改成 xx 会使得数组的和增加 2x2x,所以这样的贪心操作是最优的。
当给定的 KK 小于等于数组中负数的个数时,我们按照上述方法从小到大依次修改每一个负数即可。但如果 KK 的值较大,那么我们不得不去修改非负数(即正数或者 00)了。由于修改 00 对数组的和不会有影响,而修改正数会使得数组的和减小,因此:
如果数组中存在 00,那么我们可以对它进行多次修改,直到把剩余的修改次数用完;
如果数组中不存在 00 并且剩余的修改次数是偶数,由于对同一个数修改两次等价于不进行修改,因此我们也可以在不减小数组的和的前提下,把修改次数用完;
如果数组中不存在 00 并且剩余的修改次数是奇数,那么我们必然需要使用单独的一次修改将一个正数变为负数(剩余的修改次数为偶数,就不会减小数组的和)。为了使得数组的和尽可能大,我们就选择那个最小的正数。
需要注意的是,在之前将负数修改为正数的过程中,可能出现了(相较于原始数组中最小的正数)更小的正数,这一点不能忽略。
细节
为了实现上面的算法,我们可以对数组进行升序排序,首先依次遍历每一个负数(将负数修改为正数),再遍历所有的数(将 00 或最小的正数进行修改)。
然而注意到本题中数组元素的范围为 [−100,100][−100,100],因此我们可以使用计数数组(桶)或者哈希表,直接统计每个元素出现的次数,再升序遍历元素的范围,这样就省去了排序需要的时间。
3.代码实现
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<Integer, Integer>();
for (int num : nums) {
freq.put(num, freq.getOrDefault(num, 0) + 1);
}
int ans = Arrays.stream(nums).sum();
for (int i = -100; i < 0; ++i) {
if (freq.containsKey(i)) {
int ops = Math.min(k, freq.get(i));
ans += (-i) * ops * 2;
freq.put(i, freq.get(i) - ops);
freq.put(-i, freq.getOrDefault(-i, 0) + ops);
k -= ops;
if (k == 0) {
break;
}
}
}
if (k > 0 && k % 2 == 1 && !freq.containsKey(0)) {
for (int i = 1; i <= 100; ++i) {
if (freq.containsKey(i)) {
ans -= i * 2;
break;
}
}
}
return ans;
}
}
二、Leetcode 134. 加油站
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.解题思路
方法一:一次遍历
思路与算法
最容易想到的解法是:从头到尾遍历每个加油站,并检查以该加油站为起点,最终能否行驶一周。我们可以通过减小被检查的加油站数目,来降低总的时间复杂度。
假设我们此前发现,从加油站 xx 出发,每经过一个加油站就加一次油(包括起始加油站),最后一个可以到达的加油站是 yy(不妨设 x<yx<y)。这就说明:
∑i=xygas[i]<∑i=xycost[i]∑i=xjgas[i]≥∑i=xjcost[i] (For all j∈[x,y)) i=x∑ygas[i]<i=x∑ycost[i]i=x∑jgas[i]≥i=x∑jcost[i] (For all j∈[x,y))
第一个式子表明无法到达加油站 yy 的下一个加油站,第二个式子表明可以到达 yy 以及 yy 之前的所有加油站。
现在,考虑任意一个位于 x,yx,y 之间的加油站 zz(包括 xx 和 yy),我们现在考察从该加油站出发,能否到达加油站 yy 的下一个加油站,也就是要判断 ∑i=zygas[i]∑i=zygas[i] 与 ∑i=zycost[i]∑i=zycost[i] 之间的大小关系。
根据上面的式子,我们得到:
∑i=zygas[i]=∑i=xygas[i]−∑i=xz−1gas[i]<∑i=xycost[i]−∑i=xz−1gas[i]<∑i=xycost[i]−∑i=xz−1cost[i]=∑i=zycost[i]i=z∑ygas[i]=i=x∑ygas[i]−i=x∑z−1gas[i]<i=x∑ycost[i]−i=x∑z−1gas[i]<i=x∑ycost[i]−i=x∑z−1cost[i]=i=z∑ycost[i]
其中不等式的第二步、第三步分别利用了上面的第一个、第二个不等式。
从上面的推导中,能够得出结论:从 x,yx,y 之间的任何一个加油站出发,都无法到达加油站 yy 的下一个加油站。
在发现了这一个性质后,算法就很清楚了:我们首先检查第 00 个加油站,并试图判断能否环绕一周;如果不能,就从第一个无法到达的加油站开始继续检查。
3.代码实现
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
int i = 0;
while (i < n) {
int sumOfGas = 0, sumOfCost = 0;
int cnt = 0;
while (cnt < n) {
int j = (i + cnt) % n;
sumOfGas += gas[j];
sumOfCost += cost[j];
if (sumOfCost > sumOfGas) {
break;
}
cnt++;
}
if (cnt == n) {
return i;
} else {
i = i + cnt + 1;
}
}
return -1;
}
}
三、Leetcode 135. 分发糖果
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
来源:力扣(LeetCode) 链接:leetcode.cn/problems/ca…
2.解题思路
方法一:两次遍历
思路及解法
我们可以将「相邻的孩子中,评分高的孩子必须获得更多的糖果」这句话拆分为两个规则,分别处理。
左规则:当 ratings[i−1]<ratings[i]ratings[i−1]<ratings[i] 时,ii 号学生的糖果数量将比 i−1i−1 号孩子的糖果数量多。
右规则:当 ratings[i]>ratings[i+1]ratings[i]>ratings[i+1] 时,ii 号学生的糖果数量将比 i+1i+1 号孩子的糖果数量多。
我们遍历该数组两次,处理出每一个学生分别满足左规则或右规则时,最少需要被分得的糖果数量。每个人最终分得的糖果数量即为这两个数量的最大值。
具体地,以左规则为例:我们从左到右遍历该数组,假设当前遍历到位置 ii,如果有 ratings[i−1]<ratings[i]ratings[i−1]<ratings[i] 那么 ii 号学生的糖果数量将比 i−1i−1 号孩子的糖果数量多,我们令 left[i]=left[i−1]+1left[i]=left[i−1]+1 即可,否则我们令 left[i]=1left[i]=1。
在实际代码中,我们先计算出左规则 leftleft 数组,在计算右规则的时候只需要用单个变量记录当前位置的右规则,同时计算答案即可。
3.代码实现
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] left = new int[n];
for (int i = 0; i < n; i++) {
if (i > 0 && ratings[i] > ratings[i - 1]) {
left[i] = left[i - 1] + 1;
} else {
left[i] = 1;
}
}
int right = 0, ret = 0;
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1 && ratings[i] > ratings[i + 1]) {
right++;
} else {
right = 1;
}
ret += Math.max(left[i], right);
}
return ret;
}
}