1005.K次取反后最大化的数组和
给你一个整数数组 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] 。
思路
这道题的贪心策略非常好想 但如果不会Java 按照绝对值排序 就会比较麻烦
默认排序
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
Arrays.sort(nums);
int index = -1;
for(int i = 0; i < nums.length; i++){
if(nums[i] >= 0){
index = i;
break;
}
}
if(index == -1){
for(int i=0; i < k && i < nums.length; i++){
nums[i] *= -1;
}
if(k <= nums.length) {
return getSum(nums);
}else{
if((k-nums.length)% 2 == 0){
return getSum(nums);
}else{
Arrays.sort(nums);
nums[0] *= -1;
return getSum(nums);
}
}
}else if(index == 0){
if(k % 2 == 0){
return getSum(nums);
}else{
nums[0] *= -1;
return getSum(nums);
}
}else{
//index 首个非负数的下标 代表 负数个数
if(index <= k){
for(int i = 0; i < index; i++ ){
nums[i] *= -1;
}
Arrays.sort(nums);
if((k - index) % 2 == 0) return getSum(nums);
else{
nums[0] *= -1;
return getSum(nums);
}
}else{
for(int i = 0; i < k; i++ ){
nums[i] *= -1;
}
return getSum(nums);
}
}
}
public int getSum(int [] nums){
int sum = 0;
for(int i : nums){
sum += i;
}
return sum;
}
}
绝对值排序
- 第一步:将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
- 第二步:从前向后遍历,遇到负数将其变为正数,同时K--
- 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
- 第四步:求和
class Solution {
public int largestSumAfterKNegations(int[] nums, int K) {
// 将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小
nums = IntStream.of(nums)
.boxed()
.sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
.mapToInt(Integer::intValue).toArray();
int len = nums.length;
for (int i = 0; i < len; i++) {
//从前向后遍历,遇到负数将其变为正数,同时K--
if (nums[i] < 0 && K > 0) {
nums[i] = -nums[i];
K--;
}
}
// 如果K还大于0,那么反复转变数值最小的元素,将K用完
if (K % 2 == 1) nums[len - 1] = -nums[len - 1];
return Arrays.stream(nums).sum();
}
}
134. 加油站
在一条环路上有 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 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。
思路
1、暴力 遍历数组 以每个元素为起点模拟一圈,时间复杂度O(n^2)
2、贪心_1
-
情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
-
情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。
-
情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
rest = gas[i] - cost[i] 为一天剩下的油。i 从0 开始累加到最后一站 记录总和与 累加的最小值。 若总和为负 一定无解;若最小值非负 则从0开始即为解; 若最小值为负数,那么 从后往前开始计算,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSum = 0;
int min = Integer.MAX_VALUE;
for(int i = 0; i< cost.length; i++){
int cur = gas[i] - cost[i];
curSum += cur;
min = Math.min(curSum, min);
}
//情况1
if(curSum < 0) return -1;
//情况2
if(min >= 0) {
System.out.println(curSum);
return 0;
}
//情况三
curSum = 0;
for(int i = gas.length - 1; i>=0; i--){
int cur = gas[i] - cost[i];
curSum += cur;
if(curSum >= - min){
return i;
}
}
return -1;
}
}
3、贪心_2
那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置。
每个加油站的剩余量rest[i]为gas[i] - cost[i]。 i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int curSum = 0;
int totalSum = 0;
int start = 0;
for(int i = 0; i < gas.length; i++){
curSum += gas[i] - cost[i];
totalSum += gas[i] - cost[i];
if(curSum < 0){
start = i + 1;
curSum = 0;
}
}
if(totalSum < 0) return -1;
return start;
}
}
总结
对于本题首先给出了暴力解法,暴力解法模拟跑一圈的过程其实比较考验代码技巧的,要对while使用的很熟练。
然后给出了两种贪心算法,对于第一种贪心方法,其实我认为就是一种直接从全局选取最优的模拟操作,思路还是很巧妙的,值得学习一下。
对于第二种贪心方法,才真正体现出贪心的精髓,用局部最优可以推出全局最优,进而求得起始位置。
135. 分发糖果
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。 - 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
示例 1:
输入:ratings = [1,0,2] 输出:5 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2] 输出:4 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
思路
确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼。
分两次计算两边 按照递增方向遍历
第一次计算右边大于左边的,从前往后遍历 ; 第二次计算左边大于右边的,从后往前遍历
反之亦可。
第一次:局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果
第二次:局部最优:取 res[i + 1] + 1 和 res[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。
代码
这是先顺 后逆的代码
class Solution {
public int candy(int[] ratings) {
//分两次计算两边 按照递增方向遍历
//第一次计算右边大于左边的,从前往后遍历
//第二次计算左边大于右边的,从后往前遍历
int len = ratings.length;
int res[] = new int [len];
res[0] = 1;
//第一次
for(int i = 1; i < len; i++){
if(ratings[i] > ratings[i-1]){
res[i] = res[i-1] + 1;
}else{
res[i] = 1;
}
}
//第二次
for(int i = len-2; i >= 0; i--){
if(ratings[i] > ratings[i+1]){
res[i] = Math.max(res[i], res[i+1] + 1);
}
}
int ans = 0;
for (int num : res) {
ans += num;
}
return ans;
}
}
这是先逆序 后顺序的代码
class Solution {
public int candy(int[] ratings) {
//分两次计算两边 按照递增方向遍历
//第一次计算右边大于左边的,从前往后遍历
//第二次计算左边大于右边的,从后往前遍历
int len = ratings.length;
int res[] = new int [len];
res[len-1] = 1;
//第一次
for(int i = len-2; i >= 0; i--){
if(ratings[i] > ratings[i+1]){
res[i] = res[i+1] + 1;
}else{
res[i] = 1;
}
}
//第二次
for(int i = 1; i < len; i++){
if(ratings[i] > ratings[i-1]){
res[i] = Math.max(res[i-1] + 1, res[i]);
}
}
int ans = 0;
for (int num : res) {
ans += num;
}
return ans;
}
}
总结
这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。
那么本题我采用了两次贪心的策略:
- 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
- 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。
这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。