LeetCode日记——【算法】贪心思想专题

  贪心算法
基本思路:
贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若
下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加使算法停止。
过程:
建立数学模型来描述问题;
把求解的问题分成若干个子问题;
对每一子问题求解,得到子问题的局部最优解; 
把子问题的解局部最优解合成原来解问题的一个解。
 
  题1:分配饼干
难度:Easy
题目描述:

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

注意:

你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。

示例 :

输入: [1,2,3], [1,1]

输出: 1

解释:

你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。

虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。

所以你应该输出1。

代码:

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        if(g==null||s==null) return 0;
        Arrays.sort(g);
        Arrays.sort(s);
        int i=0,j=0;
        while(i < g.length &&  j < s.length){
            if(g[i]<=s[j]){
                i++;
            }
            j++;           
        }
        return i;
    }
}

分析:

先把胃口序列和饼干序列都排好序。

设计两个指针,一开始分别指向两个数组的头部。

比较两个指针指向的数,如果当前饼干能满足孩子,就跳到下一个孩子,下一块饼干。

如果不能满足,换下一块饼干,孩子胃口指针不动。(这块不能满足,就看看下一块更大的饼干能不能满足)。

如果饼干没有了,或者孩子没有了,都退出循环。

最后返回孩子胃口的索引。

最后返回的i,如果i从1开始,本来应该是i-1个孩子,但是数组下标是从0开始的,一抵消,就相当于直接返回i。

 

 

  题2:无重叠区间
难度:Medium
题目描述:

给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

注意:

可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:

输入: [ [1,2], [2,3], [3,4], [1,3] ]

输出: 1

解释: 移除 [1,3] 后,剩下的区间没有重叠。

代码:

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        //intervals是一个数组,数组元素是区间
        //若intervals为空,则直接返回0
        if (intervals.length == 0) {
            return 0;
        }
        //按区间的结尾从小到大进行排序
        Arrays.sort(intervals, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[1] - o2[1];
            }
        });
        int cnt = 1;
        //end定义为第一个区间的结尾
        int end = intervals[0][1];
        //遍历第2个区间到最后一个区间
        for (int i = 1; i < intervals.length; i++) {
            //若当前区间的开头比end小,就直接跳下一个区间
            if (intervals[i][0] < end) {
                continue;
        }
        //若当前区间与end所在区间没有重叠,则更新end为当前区间的末尾,计数++
        end = intervals[i][1];
        cnt++;
        }
        return intervals.length - cnt;
    }
}

分析:

这里重写了sort方法,目的是按区间尾部从小到大排序。

基本思想是记录第一个区间的尾部,然后从第二个区间开始,比较第二个区间与第一个区间是否有重叠。

若有重叠,直接跳过第二个区间,看第三个区间,比较第三个区间与第一个区间是否有重叠。

若无重叠,更新end值为第二个区间的尾部,然后计数++。然后接着比较第三个区间与第二个区间是否有重叠。

以此类推。最后返回总区间个数与计数的差值。

 

  题3:投飞镖刺破气球
难度:Medium
题目描述:

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

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

Example:

输入:
[[10,16], [2,8], [1,6], [7,12]]

输出:
2

解释:
对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。

代码:

class Solution {
    public int findMinArrowShots(int[][] points) {
        if(points.length==0) return 0;
        Arrays.sort(points, Comparator.comparingInt(o -> o[1]));
        int cnt=1;
        int end = points[0][1];
        for(int i=1;i<points.length;i++){
            //注意这里要取等号!!
            if(points[i][0]<=end){
                continue;
            }
            end=points[i][1];
            cnt++;
        }
        return cnt;
    }
}

分析:

思路与上一道基本相同,sort方法使用了lambda表达式来简化代码。

不同的是,若判断到当前区间与上一个end表示的区间有重叠,这里接壤也算有重叠(意思就是接壤也算能用一根剑刺破了)所以要取等。但是上一道接壤是不算重叠的,所以不取等。

 

  题4:根据身高和序号重组队列
难度:Medium
题目描述:

假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。

注意:
总人数少于1100人。

示例

输入:
[[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]

输出:
[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]

代码:

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        if(people==null||people.length==0||people[0].length==0) return new int[0][0];
        //按身高降序排列;若身高相同,按k升序排列
        Arrays.sort(people, (a, b) -> (a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]));
        List<int[]> queue = new ArrayList<>();
        //遍历people数组
        for(int[] p:people){
            //p[1]表示遍历到的人的k值,把遍历到的学生插入队列第k个位置
            queue.add(p[1], p);
        }
        //新建一个数组arry来存放排列好的人
        int [][] arry = new int[queue.size()][];
        //把队列转换成数组
        queue.toArray(arry);
        //返回数组arry
        return arry;
    }
}

分析:

差点没看懂题意。用那个例子来理解一下:

[[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]  第一位身高为5的人的前面没人比他高所以k为0;第二位身高为7的人的前面也没人比他高所以k为0;第三位身高为5的人的前面有2人比他高或者一样高所以k为2;第四位身高为6的人的前面有1人比他高所以k为1;第五位身高为4的人的前面有4人比他高所以k为4;第六位身高为7的人的前面有1人比他高或一样高所以k为1。

基本思路就是:先将原数组按身高降序,身高一样按k升序。建立一个队列,然后遍历数组,从头开始分别把人塞到第k位,保持当前塞的人是第k位即可。最后要把队列转化为二维数组再返回。

 

  题5:买卖股票的最大收益
难度:Easy
题目描述:

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

 示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

代码:

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if (n == 0) return 0;
        //将当前最低价初始化为第一个元素
        int soFarMin = prices[0];
        int max = 0;
        for (int i = 1; i < n; i++) {
            //若当前价格比soFarMin低,就更新soFarMin
            if (soFarMin > prices[i]) soFarMin = prices[i];
            //若当前价格比soFarMin高或相等,将当前最高利润更新为当前价格与soFarMin的差值
            else max = Math.max(max, prices[i] - soFarMin);
        }
        return max;
    }
}

 分析:

只要记录前面的最小价格,将这个最小价格作为买入价格,然后将当前的价格作为售出价格,查看当前收益是不是最大收益。

 

  题6:买卖股票的最大收益 II
难度:Easy
题目描述:

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

 示例 1:

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

代码:

class Solution {
    public int maxProfit(int[] prices) {
        int profit=0;
        for(int i=1;i<prices.length;i++){
            if(prices[i]>prices[i-1]){
                profit+=(prices[i]-prices[i-1]);
            }
        }
        return profit;        
    }
}

分析:

当访问到一个 prices[i] 且 prices[i] - prices[i-1] > 0,那么就把 prices[i] - prices[i-1] 添加到收益中。

 

  题7:种花问题
难度:Easy
题目描述:

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

给定一个花坛(表示为一个数组包含0和1,其中0表示没种植花,1表示种植了花),和一个数 n 。能否在不打破种植规则的情况下种入 n 朵花?能则返回True,不能则返回False。

示例 1:

输入: flowerbed = [1,0,0,0,1], n = 1
输出: True

代码:

class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int len = flowerbed.length;
        int cnt = 0;
        for(int i=0; i<len && cnt<n; i++){
            if(flowerbed[i]==1){
                continue;
            }
            int pre=i==0?0:flowerbed[i-1];
            int next=i==len-1?0:flowerbed[i+1];
            if(pre==0&&next==0){
                cnt++;
                flowerbed[i]=1;
            }
        }
        return cnt>=n;
    }
}

分析:

 遍历数组中的每个元素,遇到1跳过,遇到0时,记录下它前面的数pre,与后面的数next(注意边界处理)。若pre和next也为0,就计数++,并将这个位置设为1(种花)。

 

 题8:判断是否为子序列
难度:Easy
题目描述:

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:
s = "abc", t = "ahbgdc"

返回 true.

代码:

class Solution {
    public boolean isSubsequence(String s, String t) {
        int index = -1;
        //遍历s中的每个字符
        for (char c : s.toCharArray()) {
            //从t中的index+1位置开始寻找字符c,并把第一次找到的索引存入index,若找不到,返回-1
            index = t.indexOf(c, index + 1);
            if (index == -1) {
                return false;
            }
        }
        return true;
    }
}

分析:

初始化index为-1。将s转化为数组,然后遍历s中的每个元素。从t中的index+1位置开始查找字符c,并把第一次找到的索引存入index。若找不到,则index=-1。index为-1则返回false。最后返回true。

 

  题9:非递减数列

难度:Easy
题目描述:

给你一个长度为 n 的整数数组,请你判断在 最多 改变 1 个元素的情况下,该数组能否变成一个非递减数列。

我们是这样定义一个非递减数列的: 对于数组中所有的 i (0 <= i <= n-2),总满足 nums[i] <= nums[i + 1]。

 示例 1:

输入: nums = [4,2,3]
输出: true
解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。

代码:

class Solution {
    public boolean checkPossibility(int[] nums) {
        //改变的元素的个数
        int cnt = 0;
        //从第2个元素开始遍历,改变元素为2即不再遍历返回false
        for (int i = 1; i < nums.length && cnt < 2; i++) {
            //若满足当前元素比上一个元素大或相等(满足非递减),就跳过继续遍历下一个元素
            if (nums[i] >= nums[i - 1]) {
                continue;
            }
            //若遇到当前元素比上一个元素小,计数++
            cnt++;
            //如果当前遍历的是第3个元素及以后的元素,且它的前面的前面的元素比它大
            if (i - 2 >= 0 && nums[i - 2] > nums[i]) {
                //将当前值设为上一个元素的值
                nums[i] = nums[i - 1];
            } else {
                //将上一个元素的值设为当前值
                nums[i - 1] = nums[i];
            }
        }
        return cnt <= 1;
    }
}

分析:

主要思路就是,设置一个计数值,然后从第二个元素开始遍历(满足计数值为0或1才遍历,因为最多只能修改一次),如果满足要求就直接遍历下一个元素,如果出现不满足的情况,计数值++,然后将nums[i-1]=nums[i],再进行下一个遍历。

还有一个比较特别的情况就是 nums[i] < nums[i - 2],修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组,只能修改 nums[i] = nums[i - 1]。

比如3,4,2。我们遍历到2,发现它比4小。如果我们把4变成2,那么3,2,2也不满足条件了,这种情况应该将2改为4,即变成3,4,4。

 

  题10:子数组最大的和

难度:Easy
题目描述:

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

代码:
class Solution {
    public int maxSubArray(int[] nums) {
        if(nums==null||nums.length==0) return 0;
        //先定义preSum和maxSum为第一个元素
        int preSum = nums[0];
        int maxSum = preSum;
        //从第2个元素开始遍历
        for(int i=1;i<nums.length;i++){
            //之前和大于0,则加上当前元素,否则更新preSum为当前元素
            preSum = preSum > 0 ? preSum + nums[i] : nums[i];
            //maxSum始终记录最大的preSum值
            maxSum = Math.max(maxSum, preSum);
        }
        //返回maxSum
        return maxSum;
    }
}

 分析:

一开始定义preSum为第一个元素的值。然后从第2个元素开始遍历。如果preSum为负数,直接舍弃,设置preSum为当前数。然后不断更新maxSum。

 

 题11:划分字母区间

难度:Easy
题目描述:

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

 示例 1:

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

代码:

class Solution {
    public List<Integer> partitionLabels(String S) {
        //创建一个容量为26的数组
        int[] lastIndexsOfChar = new int[26];
        //遍历S中的每一个字符
        for (int i = 0; i < S.length(); i++) {
            //lastIndexsOfChar[i处的字符的编号]=i
            //这里生成的数组表示a~z每个字母最后出现的位置
            //如daeab,则数组为[3,4, ,0,2,......]
            lastIndexsOfChar[char2Index(S.charAt(i))] = i;
        }
        List<Integer> partitions = new ArrayList<>();
        int firstIndex = 0;
        while (firstIndex < S.length()) {
            int lastIndex = firstIndex;
            //从firstindex遍历到lastindex
            for (int i = firstIndex; i < S.length() && i <= lastIndex; i++) {
                //index存放当前字母最后一次出现的位置的索引
                int index = lastIndexsOfChar[char2Index(S.charAt(i))];
                if (index > lastIndex) {
                    //lastIndex始终为遍历过的字母中最后出现的索引
                    //如遍历到daeab中的第一个a时,lastIndex就为3了,直到遍历到b,lastIndex才更新为4
                    lastIndex = index;
                }
            }
            partitions.add(lastIndex - firstIndex + 1);
            firstIndex = lastIndex + 1;
        }
        return partitions;
    }
    //返回字符的编号,a为0,z为25
    private int char2Index(char c) {
        return c - 'a';
    }
}

分析:

这也太难了吧,啥玩意儿

 

贪心思想专题完结撒花~

 

 

posted @ 2020-06-11 21:08  菅兮徽音  阅读(209)  评论(0编辑  收藏  举报