贪心算法专题(二)

文章目录

一、最优除法

Leetcode链接

给定一正整数数组 nums,nums 中的相邻整数将进行浮点除法。

例如,nums = 2,3,4,我们将求表达式的值 "2/3/4"。

但是,你可以在任意位置添加任意数目的括号,来改变算数的优先级。你需要找出怎么添加括号,以便计算后的表达式的值为最大值。

以字符串格式返回具有最大值的对应表达式。

注意:你的表达式不应该包含多余的括号。

示例 1:

输入: 1000,100,10,2

输出: "1000/(100/10/2)"

解释: 1000/(100/10/2) = 1000/((100/10)/2) = 200

但是,以下加粗的括号 "1000/( ( 100/10 ) /2)" 是冗余的,

因为他们并不影响操作的优先级,所以你需要返回 "1000/(100/10/2)"。

解题思路

  • 所有a/b/c/d/e/f这样的形式都可以写成x/y的形式,一部分是分子,一部分是分母,不另加括号,那 a 就是分母,b * c * d * e * f 就是分母,那如果想使整个分式最大,就要想办法使分子变大或使分母变小。
  • 通过加括号的方法就可以使一部分数从分母的位置换到分子的位置,就可以使分式变大了。比如:a/(b/c)--->x/y,x=a*c,y=b,c就被挪到分子上去了。

贪心策略:那么对于a/b/c/d/e/f,其实只要把第一个数之后的所有数都括起来就可以把从第3个数开始的所有数都挪到分子上了:a/(b/c/d/e/f)--->x/y,x=a * c * d * e * f,y=b,这样一来就是分式所能达到的最大值,用 StringBuffer 进行拼接答案就行(要注意的是无论怎么添加括号,a永远只能在分子上,b永远只能在分母上)

代码实现及解析

java 复制代码
class Solution {
    public String optimalDivision(int[] nums) {
        int n=nums.length;
        StringBuffer ret=new StringBuffer();
        if(n==1) return ret.append(nums[0]).toString();
        if(n==2) return ret.append(nums[0]).append("/").append(nums[1]).toString();

        ret.append(nums[0]).append("/").append('(').append(nums[1]);
        for(int i=2;i<n;i++){
            ret.append("/").append(nums[i]);
        }
        ret.append(")");
        return ret.toString();
    }
}

总结

  • 复习解题思路

二、跳跃游戏II

Leetcode链接

给定一个长度为 n 的 0 索引整数数组 nums。初始位置在下标 0。

每个元素 numsi 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 (i + j) 处:

0 <= j <= numsi

i + j < n

返回到达 n - 1 的最小跳跃次数。测试用例保证可以到达 n - 1。

示例 1:

输入: nums = 2,3,1,1,4

输出: 2

解释: 跳到最后一个位置的最小跳跃数是 2。

从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

解题思路

  • 本题可以使用dp算法解决,但是时间复杂度还是O(n2),虽然在dp算法中并不算过分,但是贪心算法却可以达到O(n)的时间复杂度
  • 第一次的起点是在0下标处的,按题意我们可以往前走任意步数 j (j<=numsi),这样的话会产生很多个第二次起点的可能,所以我们干脆把在这个游戏中产生的所有可能的起点按其产生的次数顺序进行分类(第一次可能的起点:0,第二次可能的起点:a、b、c,...),统计出每个分类中那些起点的区间,看哪次区间会触碰到数组的边界。这个计算过程非常类似于层序遍历:先计算出第一次的区间--->再由第一次的这些起点统计出第二次所有可能的起点--->...,而且每个分类中那些起点的区间是可以直接计算出的,不用一一统计:
  • 可以看到第二次和第三次这样的相邻区间可能会有重叠,那肯定保留第二次的就行了,毕竟2次就能达到肯定不会选3次,所以下一次的区间的左边界newleft=right+1,而右边界就要遍历一下该区间的起点算一下所能到达的最远的点,这样也就是一遍遍历就可以解决问题了

代码实现及解析

java 复制代码
class Solution {
    public int jump(int[] nums) {
        int left=0,right=0,nextRight=0,n=nums.length;
        int count=0;//记录跳跃次数
        while(right<n-1){//每次进入循环都会更新一次区间,直到区间触碰到了数组边界
            if(left>right) return -1;//如果出现一段区间内的值含有0,那么就有可能没法更新下一个区间了(所有的方案都只能跳到那个"0"位置,然后就没法再向后走了)
            //,且之后计算出的新的区间边界会是left>right,特判一下

            for(int i=left;i<=right;i++){//找到下一个区间的右边界
                nextRight=Math.max(nextRight,i+nums[i]);
            }
            //更新左、右边界:
            left=right+1;
            right=nextRight;
            count++;//计数
        }
        return count;
    }
}

总结

  • 复习解题思路和代码注释

三、加油站

Leetcode链接

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gasi 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 costi 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

解题思路

  • 暴力解法(超时):挨个枚举所有的起点,看看哪个起点可以顺利走完一圈。有三个关键变量:rest:在一个站点(index位置)剩余的油量,gas:在这个站点可以加多少油,cost:走到下一个站点(index+1)需要消耗多少油量,每次计算一下走到下一个站点(index+1)的净收益:rest=rest+gas-cost,若rest<0,则无法到达下一个站点

  • 以上diff为净收益表,以0下标为起点举例:a、b、c...均为为从当前站点到下一站点的净收益值,假设走到 g 站点刚好净收益变为负数(a+b+...+f<0,这是一个很重要的信息,说明在到g站点之前的净收益都是正的:a+b、a+b+c、...它们均>0),那此时0作为起点已经失败,暴力解法是要继续枚举的,但是我们发现如果继续从1下标位置开始枚举是无意义的,因为那样的净收益会是:b+...+f或c+...+f或d+...+f这样的,它们均<0,因为与之前以0下标为起点相比,他们的净收益只会少了前面的一些值(见上图),而这些值已经被证明为正数了。所以当一个起点走到某个站点(index)而无法继续走到index+1时,后续的起点就从index+1开始枚举就行了。这就是由题目规律得出的贪心策略

代码实现及解析

java 复制代码
class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n=gas.length;
        int start=0;
        while(start<n){//枚举起点
            int step=0,rest=0;
            while(step<n){//以该start起点看看能往前走多少步
                int index=(start+step)%n;//环形遍历的技巧
                rest=rest+gas[index]-cost[index];//计算走到index前面的位置的净收益为多少:剩余油量+gas---去到index+1要耗的油cost
                if(rest<0) break;//发现净收益为负,则走不到index的前面位置也就是index+1位置,去尝试下一个起点
                step++;//没有break,说明一直走到了index+1位置,那就继续往前走
            }
            if(rest>=0) return start;//循环跳出是因为走完了一圈,那返回该起点
            start+=step+1;//循环跳出是因为break,那就令起点直接跳到index+1
        }
        return -1;
    }
}

总结

  • 复习解题思路

四、坏了的计算器

Leetcode链接

在显示着数字 startValue 的坏计算器上,我们可以执行以下两种操作:

双倍(Double):将显示屏上的数字乘 2;

递减(Decrement):将显示屏上的数字减 1 。

给定两个整数 startValue 和 target 。返回显示数字 target 所需的最小操作数。

解题思路

正向推导:

  • 可以看到按正向推导的思路的话,想利用x2快速接近目标值,但是其实在其中的某一步先选择-1操作才会得到更少的操作数,所以正向思维很难解决问题了,那我们就可以使用正难则反的思路

正难则反:

  • 逆向去计算从target回退到startValue最少也要多少操作次数,这个时候是操作变成了÷2和+1,而且在分情况讨论每一步的最优操作时是可以准确定位的,如上图:

首先回退过程中如遇奇数则只能采取+1操作,因为该奇数是不可能为之前的数×2得来的的,只能是由-1得来的

遇偶数时的分情况讨论:

  1. end(target)>begin(startValue)

选择÷2是最优的,证明:

  • 如果选择先+1,可能不会一直+,不然只会越变越大,一定会在某次选择÷2,假设执行了k次+1后÷2,则到达了(x+k)/2,共计(k+1)次操作
  • 而如果选择先÷2到达x/2,这时再+(k/2)即可到达(x+k)/2,共计(1+(k/2))次操作,那么可以看到到达同样的数,选择先执行÷2操作,进行的操作次数是更少的,符合题意要求
  1. end<=begin
    此时的偶数想要到达begin,就只能执行(startValue-target)次+1操作了

代码实现及解析

java 复制代码
class Solution {
    public int brokenCalc(int startValue, int target) {
        //正难则反:

        int ret=0;//记录操作次数
        //先处理target>startValue的情况:
        while(target>startValue){
            if(target%2==0) target/=2;
            else target+=1;
            ret++;
        }//循环结束之后,变为target<=startValue
        //再接着处理这种情况:
        return ret+(startValue-target);
    }
}

总结

  • 复习解题思路

五、合并区间

Leetcode链接

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervalsi = starti, endi 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

解题思路

区间问题:

①:先对区间进行排序,以左/右端点为标准都行

②:对于排序后的结果,总结出一些区间之间的一些联系、规律,进而得出解题思路

以左端点排序为例:

  • 可以得到以下性质:能够合并的区间都是连续的,一旦在按顺序合并的过程中中断了,就可以把本次合并的结果add了,再以下一个区间为起点继续向后尝试进行合并。因为合并中断说明arr1与这个新区间没有重叠,那么后面的区间在数轴上就会处于更远的距离(因为已经排过序了),就更不可能有重叠部分了。

合并方法:

  • 设起点区间为left,right,待合并区间为a,b,有重叠的两个区间需满足:right>=a,否则没有重叠。
  • 重叠后的边界,left不变,right=max(right,b),如果right 也 >=b,那就把这个待合并区间给包起来了,新的右区间应仍为right

代码实现及解析

java 复制代码
class Solution {
    public int[][] merge(int[][] intervals) {
        //1.排序(以左端点):
        Arrays.sort(intervals,(arr1,arr2)->arr1[0]-arr2[0]);
        //2.合并区间:
        int left=intervals[0][0],right=intervals[0][1],n=intervals.length;
        ArrayList<int[]> ret=new ArrayList<>();//这里顺序表储存的元素是一个数组,所以可以写<int[]>
        //可以理解为它可以储存不规则数组,但是实际上ArrayList本质上还是储存的一个引用而已
        for(int i=1;i<n;i++){
            int a=intervals[i][0],b=intervals[i][1];
            if(right>=a){//有重合,进行合并
                right=Math.max(right,b);
            }else{//不能合并了就返回结果,并更新起点继续往后遍历合并
                ret.add(new int[]{left,right});
                left=a;right=b;
            }
        }
        ret.add(new int[]{left,right});//合并到最后会把最后一个区间给漏掉(可以是intervals中真正的最后一个区间,也可能是合并了多次的一个区间,反正剩到最后了)
        //因为更新完边界之后循环就结束了,没法到下次循环中把该区间add了
        return ret.toArray(new int[0][]);
    }
}

总结

  • 复习解题思路和代码注释

List包装类中toArray()方法的使用:

无参重载方法:

list.toArray():返回的是Object\[\],拿出来后也不方便使用,不推荐

有参数的重载方法:

  • List<Integer>转化为Integer[]写法:list.toArray(new Integer[0]),注意List的泛型类型是什么就只能转化为什么样类型的数组,所以此时无法转化为int\[\]数组(可以先转Integer,再遍历转化为int),且数组长度应写为0,其他写法可能会出错
  • 那么本题的是ret.toArray(new int[0][])为正确写法,ArrayList的泛型类型为int\[\],所以可以转化为数组元素为int\[\]类型的数组,也就是二维数组,所以new int[0][],且数组长度标为了0
相关推荐
猎头南楼1 小时前
具身智能模型部署方向的能力要求与技术栈梳理
人工智能·深度学习·算法
啥都想学点的研究生2 小时前
一篇文章讲清楚:线性回归问题的求解——正规方程法
算法·机器学习·线性回归
青少儿编程课堂2 小时前
树状数组精讲:逆序对计数与离散化(附双语言解法)
算法·树状数组·离散化·信息学奥赛·逆序对
血小板要健康2 小时前
网格 dfs 与 FloodFill:从岛屿、区域到搜索路径
笔记·算法·leetcode·深度优先
VALENIAN瓦伦尼安教学设备3 小时前
设备状态检测振动分析实训台案例分析
大数据·数据库·人工智能·嵌入式硬件·算法
小小晓.3 小时前
C++ 值类别与完美转发深度剖析:从左值右值到 std::forward 源码解密
开发语言·c++·算法
怕浪猫3 小时前
用了两年 AI 编程工具后,我重新理解了什么是「资深工程师」
算法·面试·架构
hetao17338374 小时前
2026-08-27~29 hetao1733837 的刷题记录
c++·算法
云淡风轻~窗明几净4 小时前
宇宙管理学猜想
算法·图论