算法通关村第十七关——贪心思想白银挑战笔记

本篇内容继续带来几个高频的贪心问题,掌握其贪心策略,并练习使用代码快速写出其贪心策略,进而提升在算法题解题中的速度。要知道,冰冻三尺非一日之寒!

1.区间问题

区间问题设计的考察点在于,如何判断区间是否发生重叠,以及重叠区间的合并。在这里,我们使用类似于“甘特图”方式,直接先判断重叠逻辑,再写出代码!(图片来自鱼骨头)

算法通关村第十七关——贪心思想白银挑战笔记_第1张图片

此外,由于区间是通过二维数组给出的,在做题的过程中要非常清晰的知晓intervals[i]表示的含义,不然在写贪心策略的代码时特别容易晕头转向,我们将三个问题归类,一并拿下它们!

1.1判断区间是否重叠

题目见LeetCode252,描述为:给定一个会议安排时间数组intervals,判断一个人能否参加这里的全部会议。(一个人在一段时间只能参加一个会议!)

题目分析:这就是经典的判断区间是否重叠的问题,也是区间问题的开胃小菜!我想画出“甘特图”之后,你已经能用眼睛看出来是否发生重叠了!别忘了,你是用眼睛看的,而人最善于的就是贪心。因此,贪心策略不言而喻:在甘特图中,从左向右比较每一个区间线段,当一个线段的尾部大于另一个区间的头部,那么这两个区间发生重叠!

关键在于,如何将“在甘特图中,从左向右比较每一个区间线段”这句贪心策略,使用代码进行描述?答:依据每个区间段的开始位置,对intervals数组进行排序!

厘清贪心策略和代码实现逻辑,直接上代码!

    public static boolean myCanAttendMeetings(int[][] intervals) {
        //先将区间按开始时间顺序排序
        Arrays.sort(intervals, new Comparator() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0] - o2[0];
            }
        });
        for (int i = 0; i < intervals.length - 1; i++) {
            if(intervals[i][1] > intervals[i + 1][0]) return false;
        }
        return true;
    }

1.2合并区间

题目见LeetCode56,描述为:当区间发生重叠后,将区间合并,返回合并后的区间。

题目分析:贪心策略和1.1完全相同,不再赘述。关键在于,如何合并两个重叠区间?现在假设区间段i和区间段i+1发生了重叠,那么合并后区间的起始位置一定区间段i的起始位置(因为贪心策略已经将区间段拍虚了),合并后区间的结束位置则是区间段i或区间段i+1较大的结束位置。

厘清贪心策略和代码实现逻辑,直接上代码!

    public static int[][] myMerge(int[][] intervals) {
        Arrays.sort(intervals, new Comparator() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0] - o2[0];
            }
        });
        int[][] res = new int[intervals.length][2];
        int index = -1;
        for (int[] interval : intervals) {
            //重叠则区间合并
            if(index != -1 && res[index][1] >= interval[0]){
                res[index][1] = Math.max(res[index][1], interval[1]);
            }else{//不重叠则不用合并区间
                res[++index] = interval;
            }
        }
        return Arrays.copyOf(res,index + 1);
    }

1.3插入区间

题目见LeetCode57,描述为:给定一个区间的集合和新区间的范围,返回合并后的区间集合(当然可能没发生合并)。

题目分析:在画出区间段集合和新区间段的甘特图后,相信我们已经可以用眼睛合成新的区间集合了!那么,贪心策略不言而喻:遍历区间段,当枚举的区间段和新区间段没有发生重叠,即左侧相离的时候,直接将区间段加入结果中;当枚举的区间段和新区间段发生重叠,那么更新新的区间段,并将最终更新的新区间段加入结果中;当枚举的区间段和新区间段没有发生重叠,即右侧相离的时候,直接将区间段加入结果中。

关键在于,如何更新新区间段?答:新区间段的起始位置=min(枚举区间段的起始位置,新区间段的起始位置);新区间段的结束位置=max(枚举区间段的结束位置,新区间段的结束位置)

​​​厘清贪心策略和代码实现逻辑,直接上代码!

    public static int[][] myInsert(int[][] intervals, int[] newInterval) {
        int[][] res = new int[intervals.length + 1][2];
        int index = 0;
        int i = 0;//记录枚举第几个区间
        //遍历左侧相离的区间段加入结果中
        while(i < intervals.length && intervals[i][1] < newInterval[0]){
            res[index++] = intervals[i++];
        }
        //重叠则合并
        while(i < intervals.length && intervals[i][0] <= newInterval[1]){
            newInterval[0] = Math.min(newInterval[0],intervals[i][0]);
            newInterval[1] = Math.max(newInterval[1],intervals[i][1]);
            i++;
        }
        res[index++] = newInterval;
        //遍历右侧相离的区间段加入结果中
        while(i < intervals.length){
            res[index++] = intervals[i++];
        }
        return Arrays.copyOf(res, index);
    }

2.字符串分割

题目见LeetCode763,描述为:字符串由小写字母组成,我们需要将该字符串划分成尽可能多的片段,同一字母最多出现在一个片段中,返回这些片段的长度。

题目分析:乍一看,我们似乎一点思路都没有。但是,拿生活中例子来说,现在学校A的计算机专业分布在2个校区,新校长上位力求改革,希望将所有的专业划分在同一个校区,杜绝同一专业跨校区的现象。类比这个题目,字符就好像是专业,区间段就好像是校区。因此,同一个字符必须只能出现在一个区间段中。那么,我们就需要找到这个字符的最远边界,准确的说找到每段区间字符的最远边界例如,区间1中有字符abcaacaa。那么区间1字符的最远边界,就是字符a的最远边界】,这就是贪心策略

关键在于,如何确定每段区间字符的最远边界?答:可以先遍历一次字符串,得到每个字符的最远边界;然后,重新遍历字符串,设置变量idx记录当前区间字符的最远边界;当idx和当前字符下标i相等时,就达到了该区间的结束位置!

​​​厘清贪心策略和代码实现逻辑,直接上代码!

    public static List myPartitionLabels(String S) {
        List res = new ArrayList<>();
        int[] edge = new int[26];//26个字符出现的最远下标
        for (int i = 0; i < S.length(); i++) {
            edge[S.charAt(i) - 'a'] = i;
        }
        int idx = 0;//一段区间内,字符出现的最远下标
        int last = -1;//上一段区间的末尾下标
        for (int i = 0; i < S.length(); i++) {
            idx = Math.max(idx, edge[S.charAt(i) - 'a']);
            //一段区间内,字符出现的最远下标和当前下标相等,则为割点
            if(idx == i){
                res.add(i - last);
                last = i;
            }
        }
        return res;
    }

3.加油站问题

题目见LeetCode134,描述为:在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

题目分析:贪心是什么?其实你当前想到的办法就是贪心!你想到了什么?答:暴力枚举!对,我们就是用暴力枚举,不过略加一点优化!贪心策略我们设置变量区间段的净油量,当净油量小于0,证明汽车从这个区间段的任何一个位置出发都不能循环一周,因此,我们直接枚举该区间段的下一个位置。事实证明,如果总的净油量大于0,汽车一定能跑完一圈,那么肯定能从一个加油站出发循环一圈,因此我们枚举的时候只需要从0号开始遍历到最后一个加油站即可!

厘清贪心策略,直接上代码!

    public static int myCanCompleteCircuit(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;
    }

OK,《算法通关村第十七关——贪心思想白银挑战笔记》结束,喜欢的朋友三联加关注!关注鱼市带给你不一样的算法小感悟!(幻听)

再次,感谢鱼骨头教官的学习路线!鱼皮的宣传!小y的陪伴!ok,拜拜,第十七关第三幕见!

你可能感兴趣的:(算法,笔记)