LeetCode精选题之贪心思想

LeetCode精选题之贪心思想

1 分发饼干–LeetCode455

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

注意:

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

示例 1:

输入: [1,2,3], [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 2:

输入: [1,2], [1,2,3]
输出: 2
解释: 
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

思路:将最小的饼干给最不贪心的小朋友,如果最小的饼干满足不了最不贪心的小朋友,就放弃这个最小的饼干。在剩下的饼干和小朋友中继续考虑。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int gi = 0, si = 0;
        while (gi < g.length && si < s.length) {
            if (s[si] >= g[gi]) {
                si++;
                gi++;
            }else {
                si++;
            }
        }
        return gi;
    }
}

另外一种贪心思想:将最大的饼干给最贪心的小朋友。如果最大的饼干都满足不了这个最贪心的小朋友,就放弃这个小朋友。在剩下的小朋友和饼干中,依旧是将最大的饼干给最贪心的小朋友。

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Integer[] gg = new Integer[g.length];
        Integer[] ss = new Integer[s.length];
        for (int i = 0; i < g.length; i++) {
            gg[i] = g[i];
        }
        for (int i = 0; i < s.length; i++) {
            ss[i] = s[i];
        }

        Arrays.sort(gg, (a,b) -> (b-a));
        Arrays.sort(ss, (a,b) -> (b-a));// 逆序排序
        int gi = 0;
        int si = 0;
        int res = 0;
        while (gi < gg.length && si < ss.length) {
            if (gg[gi] <= ss[si]) {
                res++;
                gi++;
                si++;
            }else {
                gi++;
            }
        }
        return res;
    }
}

在以上的解法中,我们只在每次分配饼干时选择一种看起来是当前最优的分配方法,但无法保证这种局部最优的分配方法最后能得到全局最优解。我们假设能得到全局最优解,并使用反证法进行证明,即假设存在一种比我们使用的贪心策略更优的最优策略。如果不存在这种最优策略,表示贪心策略就是最优策略,得到的解也就是全局最优解。

证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,可以给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。

注:以上证明来自 CyC2018

2 无重叠区间–LeetCode435(Medium)

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

注意:

  • 可以认为区间的终点总是大于它的起点。
  • 区间 [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
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

贪心思想:对于每次选择,区间的结尾很重要。结尾越小,留给后面区间越大的空间,后面越有可能容纳更多区间。所以按照区间的结尾排序,每次选择结尾最早的,且和前一个区间不重叠的区间。

class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        int len = intervals.length;
        if (len == 0) return 0;

        Arrays.sort(intervals, (a, b) -> (a[1] == b[1] ? a[0]-b[0] : a[1]-b[1]));
        int sum = 1;// 含义:构成最长无重叠区间的区间个数
        int pre = 0;
        for (int i = 1; i < len; i++) {
            if (intervals[i][0] >= intervals[pre][1]) {
                sum++;
                pre = i;
            }
        }
        return len - sum;
    }
}

问题:是否可以用动态规划来求解的题目都可以用贪心法来求解?
这显然是不行的,举出反例即可。只有具有贪心选择性质,才能使用贪心法来求解。使用反证法来证明上面这个问题具有贪心选择性质。

某次选择的是[s(i), f(i)],其中f(i)是当前所有选择中结尾最早的。假设这个选择不是最优的,并且假设最优解在这一步选择[s(j), f(j)]f(j) > f(i)。此时,显然可以将 [s(i), f(i)]替换[s(j), f(j)],而不影响后续的区间选择。也就是说 [s(i), f(i)]可以替换[s(j), f(j)]成为一个最优解,与 [s(i), f(i)]不是最优解的假设矛盾。因此这个问题具有贪心选择性质。

总体思路:贪心算法为A;最优算法为O;发现A完全能替代O,且不影响求出最优解。

3 用最少数量的箭引爆气球–LeetCode452(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(射爆另外两个气球)。

思路:是否需要新的箭关键在于区间的末尾,所以首先需要对区间排序,排序规则是区间末尾升序,末尾相同的话区间起始点升序。
代码中的end标记,它表示:在遍历的过程中使用当前这只箭能够击穿所有气球的最远距离。这个最远距离,在每遍历一个新区间的时候,都会检查一下。结合图示理解end的含义。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-utTt3GpN-1594738258842)(.\images\LeetCode452-引爆气球示意图.png)]

参考题解:liweiwei1419:贪心算法(Python 代码、Java 代码)

class Solution {
    public int findMinArrowShots(int[][] points) {
        if (points == null || points.length == 0) {
            return 0;
        }
        Arrays.sort(points, (a, b) -> (a[1]==b[1] ? a[0]-b[0] : a[1]-b[1]));
        int minCount = 1;
        int end = points[0][1];// 表示最远距离
        for (int i = 1; i < points.length; i++) {
            if (points[i][0] > end) {
                minCount++;
                end = points[i][1];
            }
        }
        return minCount;
    }
}

4 根据身高重建队列–LeetCode406(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]]

思路:先排序再插入。排序规则:先按高度h降序,如果高度相同,则按照k升序。插入的过程就是:遍历排序之后的数组,将每个整数对插入到k位置(这个位置是指数组中的位置)上,也就是说k就是这个整数对在数组中应该在的位置。

为什么k有这样的性质?
首先题目定义k是排在这个人前面且身高大于或等于h的人数,而且对数组进行了身高的降序排序。所以对于某个整数对,他的前面肯定都是比他高的,那么把他插入到k这个位置,他的前面也就有k个人,也就符合了k的定义。

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        if (people == null || people.length == 0) {
            return people;
        }
        Arrays.sort(people, (a, b) -> (a[0]==b[0] ? a[1]-b[1] : b[0]-a[0]));
        for (int i = 1; i < people.length; i++) {
            int pos = people[i][1];// 这个人实际应该在的位置
            int[] copy = people[i];// 保存这个人的一个副本
            // 往前移位操作
            int j = i;
            for (; j > pos; j--) {
                people[j] = people[j-1];
            }
            people[pos] = copy;
        }
        return people;
    }
}

5 买卖股票的最佳时机–LeetCode121

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。注意:你不能在买入股票前卖出股票。

示例 1:

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

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

思路:代码中min记录的是前i个元素中的最小值,如果第i天的股票价格高于min就会产生利润,最后得出一个最大利润。

class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) {
            return 0;
        }
        int min = prices[0];
        int res = 0;
        for (int i = 1; i < prices.length; i++) {
            if (prices[i] > min) {
                res = Math.max(res, prices[i]-min);
            }else {
                min = prices[i];
            }
        }
        return res;
    }
}

6 买卖股票的最佳时机 II–LeetCode122

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

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

示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 3 * 10 ^ 4
  • 0 <= prices[i] <= 10 ^ 4

思路:遍历整个股票交易日价格列表 prices,策略是所有上涨交易日都买卖(赚到所有利润),所有下降交易日都不买卖(永不亏钱)。

注意:对于连续上涨交易日的情况,设此上涨交易日股票价格分别为 p1,p2,…,pn,则第一天买最后一天卖收益最大,即 pn−p1等价于每天都买卖,即 pn−p1=(p2−p1)+(p3−p2)+…+(pn−pn−1)。

参考题解:Krahets:买卖股票的最佳时机 II (贪心,清晰图解)

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

7 种花问题–LeetCode605

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

给定一个花坛(表示为一个数组包含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. 数组内已种好的花不会违反种植规则。
  2. 输入的数组长度范围为 [1, 20000]。
  3. n 是非负整数,且不会超过输入数组的大小。
class Solution {
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
        int len = flowerbed.length;
        int count = 0;
        for (int i = 0; i < len; i++) {
            if (flowerbed[i]==0 
                && (i==0 || flowerbed[i-1]==0) 
                && (i==len-1 || flowerbed[i+1]==0)) {
                count++;
                flowerbed[i] = 1;
            }
        }
        return count>=n;
    }
}

以上解法参考官方题解,边界条件写得太棒了。

if (flowerbed[i]==0 && (i==0 || flowerbed[i-1]==0) && (i==len-1 || flowerbed[i+1]==0))

对于首尾需要特别考虑,还有一种思路,在开头的左边和结尾的右边分别补零,这样首尾就可以和中间元素一起考虑了。

8 判断子序列–LeetCode392(Medium)

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

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

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

示例 1:

s = "abc", t = "ahbgdc"
返回 true.

示例 2:

s = "axc", t = "ahbgdc"
返回 false.

后续挑战 : 如果有大量输入的 S,称作S1, S2, ... , Sk其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

class Solution {
    public boolean isSubsequence(String s, String t) {
        int si = 0;
        int sLen = s.length();
        if (sLen == 0) return true;
        int tLen = t.length();
        for (int i = 0; i < tLen; i++) {
            if (t.charAt(i) == s.charAt(si)) {
                si++;
            }
            if (si == sLen) return true;
        }
        return false;
    }
}

9 非递减数列–LeetCode665

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

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

示例 1:

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

示例 2:

输入: nums = [4,2,1]
输出: false
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。

说明:

  • 1 <= n <= 10 ^ 4
  • -10 ^ 5 <= nums[i] <= 10 ^ 5

思路:
1、当数组长度小于3时,最多需要调整一次就能满足条件,返回true;
2、当数组长度大于等于3时,出现前一个元素y大于后一个元素z时,如果y的前元素x不存在,让y=z即可;若x存在,根据x和z的大小关系,做如下调整:若x>z,就让z=y,否则让y=z。

参考题解:but的题解

class Solution {
    public boolean checkPossibility(int[] nums) {
        int len = nums.length;
        if (len < 3) {
            return true;
        }
        int cnt = 0;
        for (int i = 0; i < len-1; i++) {
            if (nums[i] > nums[i+1]) {
                cnt++;
                if (cnt > 1) {
                    break;
                }
                if ((i-1>=0) && nums[i-1] > nums[i+1]) {
                    nums[i+1] = nums[i];
                }else {
                    nums[i] = nums[i+1];
                }
            }
        }
        return cnt <= 1;
    }
}

10 最大子序和–LeetCode53

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

示例:

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

进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

class Solution {
    public int maxSubArray(int[] nums) {
        if (nums == null || nums.length == 0) {
            return Integer.MIN_VALUE;
        }
        int maxSum = nums[0];
        int subSum = nums[0];
        for (int i = 1; i < nums.length; i++) {
            if (subSum < 0) {
                subSum = nums[i];
            }else {
                subSum += nums[i];
            }
            if (subSum > maxSum) {
                maxSum = subSum;
            }
        }
        return maxSum;
    }
}

11 划分字母区间–LeetCode763(Medium)

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

示例 1:

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

提示:

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

思路:对于遇到的每一个字母,去找这个字母最后一次出现的位置,用来更新当前的最小区间。

class Solution {
    public List<Integer> partitionLabels(String S) {
        // 首先记录下每个字符最后一次出现的位置
        int[] last = new int[26];
        for (int i = 0; i < S.length(); i++) {
            last[S.charAt(i)-'a'] = i;
        }

        // 根据字符最后一次出现的位置来确定区间
        List<Integer> res = new ArrayList<>();
        int start = 0, end = 0;
        for (int i = 0; i < S.length(); i++) {
            end = Math.max(end, last[S.charAt(i)-'a']);
            if (i == end) {
                res.add(end-start+1);
                start = i+1;
            }
        }
        return res;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值