【力扣一刷】代码随想录day34(贪心算法part3:1005.K次取反后最大化的数组和、134. 加油站、135. 分发糖果)

【1005.K次取反后最大化的数组和】简单题

方法一  直接从小到大排序(需考虑全部情况,容易出错)

思路:

1、考虑直接从小到大排序后的所有可能情况:全是负数、先负数后正数、全是正数

2、处理特殊情况

特例1:全是负数,且k > nums.length

[-8, -2, -1], k = 6  ->  结果9

处理逻辑:遍历到最后一个负数,取反后,次数还剩奇数次,再对最后一个负数取反

特例2:先负数后正数,且k>nums.length

[-7, -6, -3, 1, 3], k = 6  ->  结果18

处理逻辑:所有的负数已取反完,且遍历到第一个正数时,次数还剩奇数次,则比较第一个最小的正数取反后的值与前一个最大的负数的值,即例子中第一个正数1取反后的-1和-3比较,-1更大,则对1取反。

特例3:全是正数

[1, 4, 6, 9], k = 11  -> 结果18

处理逻辑:所有次数只用在第一个正数上,如果k为奇数,则对第一个正数取反,如果k为偶数,则所有数维持原状。

class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        Arrays.sort(nums); // 直接从小到大排序
        int sum = 0;
        for (int i = 0; i < nums.length; i++){
            // 如果当前遍历的元素是负数
            if (nums[i] <= 0){
                // 如果次数没用完,继续处理
                if (k > 0) {
                    nums[i] = -nums[i];
                    k--;
                    // 全是负数,且次数用不完的情况
                    if (i == nums.length - 1 && k % 2 == 1){
                        nums[i] = -nums[i];
                        break;
                    }
                }
                // 如果次数用完,直接退出遍历
                else break;
            }
            // 如果当前遍历的元素是正数
            else{
                // 判断剩余次数是奇数还是偶数,奇数继续处理后退出遍历,偶数可以直接退出遍历
                if (k % 2 == 1){
                    // 如果第一个元素就是正数,即全是正数的情况,且k是奇数,则对第一个元素取反后结束
                    if (i == 0) nums[0] = -nums[0]; 
                    // 如果不是第一个元素是正数,即先负数后正数的情况
                    else {
                        if (-nums[i] > -nums[i-1]) nums[i] = -nums[i];
                        else nums[i-1] = - nums[i-1];
                    }
                }
                break;
            }
        }

        for (int i = 0; i < nums.length; i++){
            sum += nums[i];
        }
        return sum;
    }
}
  • 时间复杂度: O(nlogn),排序
  • 空间复杂度: O(1)

方法二  按绝对值从大到小排序(更推荐,特殊情况少)

思路:

1、按绝对值从大到小排序

2、for循环先对排在前面的负数进行取反,尽可能地使数组和变大

3、处理特殊情况

如果所有的负数取反完(包含一开始就全是正数的情况),次数还剩奇数次,则对最后一个绝对值最小的正数取反

注意:对nums数组,不能直接使用Array.sort重写Comparator接口的compare方法的方式进行绝对值的降序排序,因为nums数组存储的是基本类型int型的数据,只有对对象进行排序才能重写compare方法。


错误示范:

Arrays.sort(nums, (Integer o1, Integer o2) -> Math.abs(o2) - Math.abs(o1))


正确示范:

nums = IntStream.of(nums)

            .boxed()  // 打包成Integer流

            .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))

            .mapToInt(o -> o.intValue()) // 或mapToInt(Integer::intValue())

            .toArray();

操作:int型数组 -> int流 -> Integer流 -> sorted中间方法排序 -> map中间方法映射回int流 -> toArray终结方法转回int型数组

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

        for (int i = 0; i < nums.length; i++){
            // 只对负数取反
            if (nums[i] < 0 && k > 0){
                nums[i] = -nums[i];
                k--;
            }
            if (k <= 0) break;
        }
        // 如果遍历完次数还没用完,还剩奇数次,就将绝对值最小的元素取反
        if (k > 0 && k % 2 == 1) nums[nums.length - 1] = - nums[nums.length - 1]; 
        
        return Arrays.stream(nums).sum();
    }
}
  • 时间复杂度: O(nlogn),排序
  • 空间复杂度: O(1)

总结:lambda表达式return的不同类型

1、静态方法

2、实例方法

3、特殊类型方法

4、构造器


【134. 加油站】中等题

方法一  暴力法 

思路:遍历每个加油站,对可能成为起点的加油站(gas[i] > cost[i])进行绕行判断

注意:只有一个加油站的情况下,也要进行绕行判断,即gas[0] >= cost[0]时能成功绕行。

关键:理解为什么 gas[i] = cost[i] 的加油站不可能成为起点?

假设从gas[i] = cost[i]的加油站出发,想要成功绕行一周,有以下三种情况:

  • 情况1,剩下的加油站全是gas[i] = cost[i]的加油站,则能成功绕行一周,那么每个gas[i] = cost[i]的加油站其实都能绕行一周,不符合题目描述的有解则必然是唯一解。
  • 情况2,假设后面有 gas[i] > cost[i] 的加油站,且能成功绕行一周,那遇到的 gas[i] > cost[i] 的加油站也可以成为起点,不符合题目描述的有解则必然是唯一解。
  • 情况3,假设后面有 gas[i] < cost[i] 的加油站,且能成功绕行一周,那么肯定是先遇到 gas[i] > cost[i] 的加油站,再遇到 gas[i] < cost[i] 的加油站,才能成功绕行,那其实先遇到的 gas[i] > cost[i] 的加油站也可以成为起点,那也不符合题目描述的有解则必然是唯一解。

综上,gas[i] = cost[i] 的加油站不可能成为起点,如果可以作为起点,则必然不止唯一解。

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        if (gas.length == 1) return gas[0] >= cost[0] ? 0 : -1;
        for (int i = 0; i < gas.length; i++){
            // 如果第i个加油站能够提供足够去第i+1个油站的油,则选第i个加油站为起点
            if (gas[i] > cost[i]){ // 注意:gas[i] = cost[i]的加油站不可能是起点
                boolean can = true; // 默认从第i个加油站出发能完整行驶一周
                int cnt = gas.length; // 记录剩余要去的加油站数
                int j = i;  // 记录目前所在的加油站
                int cur = 0;  // 记录当前油量
                // 从第i个加油站开始,加上回到起点,一共要经过gas.length个加油站
                while (cnt-- > 0){
                    if (j >= gas.length) j = 0; // 如果索引越界则从0开始
                    cur = cur + gas[j] - cost[j]; // 计算在当前加油站加油以及去完下一个加油站后的剩余油量
                    if (cur < 0) { // 如果剩余油量<0,证明到不了下一个加油站
                        can = false;
                        break;
                    }
                    j++;
                }
                // 如果第i个加油站可以作为起点,则直接返回i,因为有解则只有唯一解
                if (can) return i;
            }
        }
        // 如果所有从所有可以出发的加油站出发,都不能绕行一周,则返回false
        return -1;
    }
}
  • 时间复杂度: O(n²),最坏的情况下,n个元素作为起点,再分别绕行一周遍历n次
  • 空间复杂度: O(1)

方法二  贪心算法(需要数学证明)

理解:

1、如何判断是否有解?

sum(gas) >= sum(cost)情况下,即所有油站加的总油量 > 绕行所有油站一圈总消耗的油量,则能成功绕行,即有解。

2、如何确定起点?

  • 利用假设法:先假设某个可能成为起点的加油站为起点,并用start标记,然后在遍历每个加油站的过程中判断该假设是否成立。若遍历到最后一个加油站,该假设还没被推翻,证明start标记的就是真正的起点(贪心算法的经典思维)。
  • 如何判断假设是否成立?每遍历一个加油站,就计算从start开始的加油站开到当前遍历的第i个加油站的剩余油量,如果剩余油量出现负数,则证明从起点start出发,无法到达第i+1个加油站,也就不能成功绕行一圈,所以假设的start不成立,需要重新假设新的start。
  • 在上一个假设不成立的情况下,如何设定新的假设?如果从start开始到第i个加油站,然后去往第i+1个加油站的过程中油量断供了,说明start不可能是起点。而且,从start到 i 区间内的所有加油站都不可能是起点,包含 start 和 i 。那么,只有从索引为 i+1 开始的加油站才可能是起点,假设新的start为 i+1 ,并重新初始化剩余油量为0即可。 
  • 为什么被推翻假设的区间内的加油站都不可能为起点?如果区间内出现起点,那么起点到第 i 个加油站的剩余量肯定是正数,而从区间的开头start开始到第 i 个加油站的剩余量是负数,说明从start到所谓区间内的起点的加油站的剩余油量肯定是负数,那么这个起点在遍历第 i 个加油站前应该已经假设过了,遍历第 i 个加油站的起点肯定不是start,出现矛盾,证明区间内不可能出现起点。 

思路:       

1、for循环从0开始遍历每个加油站,用start标记可能成为起点的加油站索引。

  • 计算从start开始到第i个加油站的剩余油量
  • 计算从0开始到第i个索引对应加油站的剩余油量(循环结束后,用于判断是否有解)
  • 如果以start为起点出发计算的某个加油站的剩余油量小于0,说明油断供了不能前往这个加油站,则[start, i]区间内的加油站都不可能作为起点,更新start为i+1,初始化从start出发的剩余油量为0。

2、判断是否有解并返回相应的结果

  • 如果供大于求(sum(gas) < sum(cost)),则无解,返回-1。
  • 否则,即有解的情况下,返回start标注的起点加油站。

优点:for循环遍历一次能同时判断是否有解,还能获取解对应的索引。

class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int start = 0; // 用于记录可能作为起点的加油站
        int restFromIndex = 0; // 用于计算从start开始到某个加油站的剩余油量
        int AllRest = 0; // 用于计算从0开始到最后一个索引对应加油站的剩余油量(用于判断是否有解)
        
        for (int i = 0; i < gas.length; i++){
            restFromIndex += gas[i] - cost[i];
            AllRest += gas[i] - cost[i];
            if (restFromIndex < 0){ // 如果以start为起点出发计算的某个加油站的剩余油量小于0,说明油断供了,跑不了
                start = i + 1; // 则[start, i]区间内的加油站都不可能作为起点,更新start为i+1
                restFromIndex = 0; // 重新计算从start出发的剩余油量
            }
        }
        if (AllRest < 0) return -1; // 如果供大于求(sum(gas) < sum(cost)),则无解,返回-1
        else return start; // 有解的情况下,返回start标注的起点加油站
    }
}
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)


【135. 分发糖果】困难题

方法  

难点:如果只从左到右遍历,可能会影响前面的小孩已成立的糖果数关系。

例如:

分数为[1, 3, 2,1],先初始化所有小孩的糖果数sugars为[1, 1, 1, 1] ,因为至少拥有1个糖果

定义的遍历规则:

详细流程:

1 < 3,1 = 1,糖果数更新为 [1, 2, 1, 1]

3 > 2,2 > 1,不需要更新糖果数

2 > 1,1 = 1,糖果数更新为 [1, 2, 2, 1]

结束遍历

错误分析:很明显遍历到索引2时,要增大索引2小孩的糖果数,使其大于索引3的小孩的糖果数,但是增大索引2小孩的糖果数后,索引1的糖果数就和索引2的糖果数一样了,不符合相邻小孩分数高的糖果数多,即这个思路不成立。

思路:既然当前遍历的孩子的糖果数需要考虑两个相邻孩子分数,那就从左右两个方向各遍历一次,从左边遍历可以保证当前孩子与左边相邻孩子的糖果数符合要求的关系,从右边遍历可以保证当前孩子与右边相邻孩子的糖果数符合要求的关系,遍历完后只要取左右两次遍历对应索引处的最大值作为真正的糖果数即可。

class Solution {
    public int candy(int[] ratings) {
        // 从左往右遍历,确保当前孩子与左邻孩子的糖果数符合分数关系
        int[] leftSugars = new int[ratings.length];
        leftSugars[0] = 1;
        for (int i = 1; i < leftSugars.length; i++){
            // 如果当前比左边分数高,则糖果数至少比左边多1,如果分数小于或等于左边,设为最小值1即可。
            leftSugars[i] = ratings[i] > ratings[i-1] ? leftSugars[i-1] + 1 : 1;
        }
        
        // 从右往左遍历,确保当前孩子与右邻孩子的糖果数符合分数关系
        int[] rightSugars = new int[ratings.length];
        rightSugars[rightSugars.length - 1] = 1;
        for (int i = rightSugars.length - 2; i >= 0; i--){
            // 如果当前比右边分数高,则糖果数至少比右边多1,如果分数小于或等于右边,设为最小值1即可。
            rightSugars[i] = ratings[i] > ratings[i+1] ? rightSugars[i+1] + 1 : 1;
        }

        // 取对应索引最大值,使当前孩子与左右邻孩子的糖果数都符合分数关系
        int[] sugars = new int[ratings.length];
        int res = 0;
        for (int i = 0; i < ratings.length; i++){
            sugars[i] = Math.max(leftSugars[i], rightSugars[i]);
            res += sugars[i];
        }
        return res;
    }
}
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值