贪心算法练习

假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。

给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false

示例 1:
输入:flowerbed = [1,0,0,0,1], n = 1
输出:true

示例 2:
输入:flowerbed = [1,0,0,0,1], n = 2
输出:false

提示:
1 <= flowerbed.length <= 2 * 104
flowerbed[i] 为 0 或 1
flowerbed 中不存在相邻的两朵花
0 <= n <= flowerbed.length

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/can-place-flowers

题目中提到在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false,表明了只要最后种下的花大于等于n就返回true,否则返回false

采用二次遍历的方式:
第一次遍历:从左往右开始遍历,如果当前这个花坛是空的,那么我们需要将判断前一个花坛是否是空的,如果是空的,表明有机会在这个花坛种花,将这个花坛标记为2,否则,如果没有机会种花则标记为3,表示虽然花坛为空,但是不可以在这个花坛种花。(获取局部最优解)
第二次遍历:从右往左开始遍历,如果当前这个花坛的值是2或者0,表示这个花坛是空的,那么就判断它的后一个花坛是否为空的,如果不为空,已经种了花,表示这个花坛不可以种花,否则,后一个花坛没有种花,那么表示这个花坛可以种花,那么就将花种在这个花坛,即将1赋值给这个下标

对应的代码:

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
       int count = 0,i;//count表示种花数量
       //从左往右开始遍历,将当前花坛和前一个花坛进行比较
       for(i = 1; i < flowerbed.length; i++){
           if(flowerbed[i] == 0){
           //当前的花坛为空,那么将判断前一个花坛是否为空,如果为空,表示有机会在这个花坛种花,否则没有
               if(flowerbed[i - 1] != 1)
                  flowerbed[i] = 2;//这个花坛有机会种花
                else
                  flowerbed[i] = 3;//表示这个花坛没有机会种花
           }
       }
       for(i = flowerbed.length - 1; i >= 0; i--){
           if(flowerbed[i] == 2 || flowerbed[i] == 0){
               /*
               当前这个花坛是2或者0,表示这个花坛有机会种花,这时候只要判断后一个花坛是否为空的即
               可,即只要它的值不为1表示花坛是空的.
               这里之所以是if(flowered[i] == 2 || flowered[i] == 0)是因为上面第一次遍历中是从下标
               1开始的,所以下标为0的花坛的值可能为0,因此这里需要右flowered[i] == 0
               */
               if(i == flowerbed.length - 1 || flowerbed[i + 1] != 1){
                  flowerbed[i] = 1;
                  count++;
               } 
           }
       }
       return count >= n;
    }
}

运行结果:
在这里插入图片描述

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:
输入:[1,0,2]
输出:5
解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。

示例 2:
输入:[1,2,2]
输出:4
解释:你可以分别给这三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/candy

二次遍历:
第一次遍历:从左往右开始遍历,将当前孩子和前一个孩子进行比较,如果当前孩子的评分比前一个孩子的评分高,那么当前孩子得到的糖果比前一个孩子多1个。(完成第一次遍历之后,得到局部最优解,但是由于要求评分最高的孩子得到的糖果要比相邻的两个孩子多,所以还需要进行第二次遍历)
第二次遍历:从右往左开始遍历,将当前孩子和后一个孩子进行比较,如果当前孩子的评分大于后一个孩子的评分,并且当前这个孩子得到的糖果小于等于后一个孩子的糖果,那么就更新当前孩子得到的糖果,变成后一个孩子的糖果加1,否则虽然当前孩子的评分大于后一个孩子的评分,但是当前孩子得到的糖果已经大于了后一个孩子的糖果数,这时候不需要进行任何操作,而不是要考虑最少,然后更新当前孩子的糖果,因为当前这个孩子得到的糖果已经是局部最优的了
请看下面的分析:
在这里插入图片描述
对应的代码:

class Solution {
    /*
    由于每一个孩子至少有1颗糖果,所以定义一个数组,表示每个孩子得到的糖果数量,
    1、将这个数组初始化为1,表示每一个孩子都能得到1个糖果。
    2、2次遍历数组,由于是评分更高的孩子必须比他两侧的领位孩子获得更多的糖果,
    所以现从左到右遍历ratings数组,如果当前下标孩子评分比右边一个孩子的评分高,
    就将当前下标孩子的糖果+1;从右往左遍历,如果当前下标孩子的评分比左边孩子的评分
    高并且当前下标孩子的糖果小于等于左边孩子的糖果,那么当前下标孩子的糖果等于左边孩子
    的糖果+1
    3、遍历数组进行累加,即可得到最少糖果
    */
    public int candy(int[] ratings) {
        int[] candys = new int[ratings.length];
        int i,res = 0;
        candys[0] = 1;
        for(i = 1; i < ratings.length; i++){
            candys[i] = 1;//初始值为1
            if(ratings[i] > ratings[i - 1])//和前一个孩子进行比较,如果当前孩子的评分更高,那么当前孩子得到的糖果是前一个孩子的糖果数加1,从而得到局部最优解
               candys[i] = candys[i - 1] + 1;
        }
        for(i = ratings.length - 2; i >= 0; i--){
        /*
        当前孩子已经取到了局部最优解了,所以如果当前的孩子评分比后一个孩子的评分高,并且当前孩子
        得到的糖果已经大于了后一个孩子的糖果,不做任何操作,否则当前孩子的糖果小于等于后一个孩子
        的糖果,那么当前孩子的糖果数就是后一个孩子的糖果数+1
        */
            if(ratings[i] > ratings[i + 1] && candys[i] <= candys[i + 1])
              candys[i] = candys[i + 1] + 1;
        }
        for(int num: candys){
            res += num;
        }
        return res;
    }
}

运行结果:
在这里插入图片描述

在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。

一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。

给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。

示例 1:
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球

示例 2:
输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4

示例 3:
输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2

示例 4:
输入:points = [[1,2]]
输出:1

示例 5:
输入:points = [[2,3],[2,3]]
输出:1

提示:

1 <= points.length <= 104
points[i].length == 2
-2^31 <= xstart < xend <= 2^31 - 1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons

定义一个公共的射箭区域[left,right],然后遍历数组points,将right和遍历到的points[ i ][ 0 ] 进行比较,如果right大于等于points[ i ] [ 0 ],这时候需要维护这个公共的射箭区域,使得left = points[ i ][ 0 ],right = Math.min(right,points[ i ][ 1 ],否则,如果当前的气球不在公共的射箭区域,那么重新定义公共的射箭区域为left = points[ i ][ 0 ],right = points[ i ][ 1 ] ,此时需要再射出一支箭

图解过程:
在这里插入图片描述

对应的代码:

class Solution {
    public int findMinArrowShots(int[][] points) {
        if(points.length == 1)
           return 1;
        int left,right,i,count = 1,len;//count表示最少射箭数目
        len = points.length;
        quickSort(points,0,len - 1);//将对points进行排序,从而使得气球按照points[i][0]升序进行排序的
        left = points[0][0];//由于实际代码中left并没有起作用,所以可以不写left,只是这样为了理解,定义一个left变量
        right = points[0][1];//假设第一个气球作为公共的射箭区域,此时射出第一支箭
        for(i = 1; i < len; i++){
            if(right >= points[i][0]){
                /*
                如果产生了交集,那么就更新射箭的范围,维护公共射箭区域
                由于数组points是按照points[i][0]升序排序的,所以如果产生交集,
                那么交集的左边界就是points[i][0],而右边界就是两者中的最小值
                */
                left = points[i][0];
                right = right < points[i][1] ? right : points[i][1];
            }else{
                //当前气球不在公共的射箭区域,此时需要再射出一支箭,并且更新公共的射箭区域
                left = points[i][0];
                right = points[i][1];
                count++;
            }
        }
        return count;
    }
    public void swap(int[][] points,int i,int j){
        int[] tmp = points[i];
        points[i] = points[j];
        points[j] = tmp;
    }
    /*
    利用三数中值法进行快数排序。取数组中首、尾、中三个数排序之后的中间数作为枢纽,这样的
    快速排序就可以避免数组一开始就是一个升序数组的情况(因为快速排序最坏情况的时间复杂度是O(n ^ 2)
    原因就是数组本身就是已经升序的了)
    */
    public int[] getPivot(int[][] points,int low,int high){
        int mid = (low + high) / 2;
        if(points[low][0] > points[mid][0])
           swap(points,low,mid);
        if(points[low][0] > points[high][0])
           swap(points,low,high);
        if(points[mid][0] > points[high][0])
           swap(points,mid,high);
        swap(points,mid,high - 1);
        return points[high - 1];
    }
    public void quickSort(int[][] points,int low,int high){
        if(low < high){
        /*
        中间枢纽的位置位于下标high - 1处,而不是中间,这是因为上面的getPivot方法中最后一步将中间枢
        纽和points[high - 1]进行交换了
        */
            int[] pivot = getPivot(points,low,high);
            int i,j;
            i = low;
            j = high - 1;
            while(true){
                while(i < j && points[++i][0] <= pivot[0]);
                while(i < j && points[--j][0] >= pivot[0]);
                if(i >= j)
                  break;
                swap(points,i,j);
            }
            swap(points,i,high - 1);
            quickSort(points,low,i - 1);
            quickSort(points,i + 1,high);
        }
    }
}

运行结果:
在这里插入图片描述

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠

示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/non-overlapping-intervals

思想和上面一道题是类似的,同样需要维护公共的区间。
对应的代码:

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        if(intervals.length == 0)
            return 0;
        int count,len,i,right;
       // int left;为了更好理解公共的区间
        len = intervals.length;
        quickSort(intervals,0,len - 1);
        count = 1;//表示最后保留在数组中不重叠的区间个数。(本题转成了剩余不重叠区间最大数量)
       // left = intervals[0][0];
        right = intervals[0][1];
        for(i = 1; i < len; i++){
           if(right > intervals[i][0]){
               /*
               如果两个区间重叠,那么需要更新区间,为了使得剩余不重叠的区间最多,需要删除两者
               中的较大区间,即right取两者的最小值。之所以是将right更新为小区间,是因为大的区间可
               能会和其他的区间重叠,从而使得数组中不重叠的区间变少。要使得剩余不重叠的区间最多,
               移除的是更大的区间。因此right取两者中的最小值
               这里的if判断不是right >= intervals[i][0],是因为这样时边界接触,并没有相互重叠。
               */
            //   left = intervals[i][0];
               right = Math.min(right,intervals[i][1]);
           }else{
               //如果没有公共交集,那么重新更新区间
             //  left = intervals[i][0];
               right = intervals[i][1];
               count++;
           }
        }
        return len - count;
    }
    /*
    利用三数中值法进行快速排序
    */
    public void swap(int[][] intervals,int i,int j){
        int[] tmp = intervals[i];
        intervals[i] = intervals[j];
        intervals[j] = tmp;
    }
    public int[] getPivot(int[][] intervals,int low,int high){
        int mid = (low + high) / 2;
        if(intervals[low][0] > intervals[mid][0])
          swap(intervals,low,mid);
        if(intervals[low][0] > intervals[high][0])
          swap(intervals,low,high);
        if(intervals[mid][0] > intervals[high][0])
          swap(intervals,mid,high);
        swap(intervals,mid,high - 1);
        return intervals[high - 1];
    }
    public void quickSort(int[][] intervals,int low,int high){
        if(low < high){
            int[] pivot = getPivot(intervals,low,high);
            int i,j;
            i = low;
            j = high - 1;
            while(true){
                while(i < j && intervals[++i][0] <= pivot[0]);
                while(i < j && intervals[--j][0] >= pivot[0]);
                if(i >= j)
                  break;
                swap(intervals,i,j);
            }
            swap(intervals,i,high - 1);
            quickSort(intervals,low,i - 1);
            quickSort(intervals,i + 1,high);
        }
    }
}

运行结果:
在这里插入图片描述

字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。

示例:
输入:S = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”, “defegde”, “hijhklij”。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 的划分是错误的,因为划分的片段数较少。

提示:
S的长度在[1, 500]之间。
S只包含小写字母 ‘a’ 到 ‘z’ 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/partition-labels
道理和跳跃游戏||是类似的。
遍历当前的字符,获取这个字符在字符串中出现的最后一个下标end。这时候从这个字符的下标from开始到最后一次出现的位置end,有可能是一个片段,但是from-end中的某一个字符,最后一次出现的位置大于end,即像这样ababcbacadb,b最后出现的位置是在a最后一次出现的位置的后面,所以需要更新end,使得end = Math.max(end,s.lastIndexOf(s.charAt(i)),否则就会导致片段出现错误。
同时注意的是,如果当前的下标等于了end,表示这个片段已经遍历结束了,这时候就可以将这个片段的长度添加到列表中,同时需要更新起始下标from,使其为from = i + 1。

对应的代码:

class Solution {
    public List<Integer> partitionLabels(String s) {
         List<Integer> list = new ArrayList<Integer>();
         int i,from = 0,end = 0,len;//from-end表示一个片段
         char ch;
         len = s.length();
         for(i = 0; i < len; i++){
            ch = s.charAt(i);
            /*
            获取当前这个字符最后一次出现的位置,那么当前位置和最后的位置可能就是一个片段,所以需要
            维护end.但是有可能从from-end中间的某一个字符最后一次出现的位置大于了end,这时候需要更
            新end,否则就会导致片段出现错误。当当前的下标是这个片段的最后一个字符的时候,那么将这
            个的长度添加到列表中,然后更新from
            */
            end = Math.max(end,s.lastIndexOf(ch));
            if(i == end){
                list.add(end - from + 1);
                from = i + 1;
            }
         }
         return list;
    }
}

运行结果:
在这里插入图片描述
优化,在获取当前字符最后一个位置的时,可以定义一个长度时26的数组,分别对应26个字母,存放这些字符最后一次出现的下标。这样的好处是不需要每次都调用s.lastIndexOf(),例如ababcbacadb中的第一个a已经知道了最后一次出现的下标,但是片段中间的a依旧会调用lastIndexOf()方法,从而增加了程序运行的时间,所以采用数组的方式获取最后一次下标(以空间换时间),从而降低时间复杂度.
优化:

class Solution {
    public List<Integer> partitionLabels(String s) {
         List<Integer> list = new ArrayList<Integer>();
         int[] last = new int[26];//存放字符最后一次出现的下标
         int i,from = 0,end = 0,len;//from-end表示一个片段
         char ch;
         len = s.length();
         for(i = 0; i < len; i++){
             ch = s.charAt(i);
             last[ch - 'a'] = i;//获取当前字符的下标
         }
         for(i = 0; i < len; i++){
            ch = s.charAt(i);
            //获取当前这个字符最后一次出现的位置,那么当前位置和最后的位置可能就是一个片段,所以需要维护farthest
            end = Math.max(end,last[ch - 'a']);
            if(i == end){
                list.add(end - from + 1);
                from = i + 1;
            }
         }
         return list;
    }
}

运行结果:
在这里插入图片描述

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例 1:

输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。
示例 2:

输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

提示:
1 <= people.length <= 2000
0 <= hi <= 106
0 <= ki < people.length
题目数据确保队列可以被重建

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/queue-reconstruction-by-height

解题思路:利用排序,对身高进行对应的优化,从而获得局部最优解。然后再进行排队,将其插入到队列中即可。
排序的原则:如果人的身高不同的时候,那么就按照身高降序排序,否则,身高相同的时候,根据人数升序排序

为什么需要将身高降序呢?

people[ i ] 表示的是身高大于等于people[ i ][ 0 ] 排在他前面的人有people[ i ][ 1 ],最后的结果也是基于这个来进行排队的。所以按照身高降序的时候,那么当前的人people[ i ] 的身高people[ i ][ 0 ]必然小于等于[ 0 , i - 1]这些人的身高,那么peole[ i ]要么放在[ 0, i - 1]这些人的前面、或者后面或者中间,取决于people[ i ][ 1 ],因为这个数强调的是排在people[ i ]这个人的前面的人数,所以只要知道了people[ i ][ 1 ],就可以知道需要将这个人排在哪里了

身高相同的时候,为什么需要将人数按照升序进行排序呢?

如果身高相同,但是人数没有按照升序排序的话,假设people[ i ]和people[ i + 1]的身高相同,但是人数不同,即如果people[ i ][ 0 ] == people[ i + 1][ 0 ],并且people[ i ][ 1 ] > people[ i + 1][ 1 ],这时候在遍历到people[ i ] 的时候,就会将其插入到队列中,此时排在people[ i ]前面有people[ i ][ 1 ]个人,但是遍历到people[ i + 1]的时候,由于people[ i + 1][ 1 ]小于people[ i ][ 1 ],那么这时候people[ i + 1]必然插入在people[ i ]的前面,这时候就不再满足people[ i ]前面有people[ i ][ 1 ]个人的身高大于等于他了,因为多出了people[ i + 1]。所以为了避免这样的错误,需要在身高相同的时候,按照people[ i ][ 1 ] 升序排序才正确

请看图解:
在这里插入图片描述

对应的代码:

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people,new Comparator<int[]>(){
             public int compare(int[] person1,int[] person2){
                 if(person1[0] != person2[0]){
                     //如果身高不同,那么按照身高降序排序
                     return person2[0] - person1[0];
                 }else{
                     //身高相同,按照人数升序排序
                     return person1[1] - person2[1];
                 }
             }
        });
         /*
         利用两个队列来实现的时候,需要注意将跳出队列的方法不是poll,而是pollLast,否则
         会导致结果的错误。
        Deque<int[]> queue1 = new LinkedList<int[]>();
        Deque<int[]> queue2 = new LinkedList<int[]>();
        List<int[]> list = new ArrayList<int[]>();
        for(int[] per: people){
            System.out.println(per[0] + " , " + per[1]);
            while(per[1] < queue1.size()){
                queue2.offer(queue1.pollLast());
            }
            queue1.offer(per);
            while(!queue2.isEmpty()){
                queue1.offer(queue2.pollLast());
            }
        }
        while(!queue1.isEmpty()){
            list.add(queue1.pollFirst());
        }
        */
        List<int[]> list = new ArrayList<int[]>();
        for(int[] person: people){
          /*
          在person[1]处插入person,这时候才可以实现前面已经有了person[1]个人身高大于等于当前
          person。然后需要将原来的person[1]及后面的人后移,给person的插入腾出位置
          */
            list.add(person[1],person);
        }
        return list.toArray(new int[list.size()][]);

    }
}

运行结果:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值