LeetCode刷题总结:贪心算法


这篇博客不会涵盖 LeetCode 所有贪心算法题目,而是根据网上相关推荐筛选出比较经典的贪心算法题目,按照由易到难的顺序介绍。

1、分发饼干

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

注意:

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


分析

要求: 满足最多的孩子
约束条件: 饼干数量;饼干尺寸;孩子胃口;每个孩子至多一块饼干

为了尽可能满足更多的孩子,因此考虑优先满足胃口小的孩子,同一个孩子可能有多个饼干可以满足他,对于胃口较小的孩子,我们选择将能够满足他的最小的饼干分给他,这样其他较大的饼干可能可以用来满足胃口更大的孩子。从而有希望满足最多的孩子。

根据以上分析,我们考虑代码,优先满足胃口较小的孩子,需要根据孩子胃口从小到大排序。胃口较小的孩子选择满足他的最小的饼干,需要对饼干按从小到大排序。 P y t h o n 3 Python3 Python3 代码具体如下:

class Solution:
    def findContentChildren(self, g, s):
        """
        :type g: List[int]
        :type s: List[int]
        :rtype: int
        """
        g.sort(); s.sort()
        count, i, j = 0, 0, 0
        while i < len(g) and j < len(s):
            if s[j] >= g[i]:        # 找到满足孩子胃口的最小的饼干
                count += 1
                i += 1; j += 1      # 开始查找能够满足下一个孩子的饼干
            else:
                j += 1
        return count

总结:

此类问题类似于双方博弈,要求一方胜利最多的策略,比如:田忌赛马问题。此类题目通常具有如下特点:

  1. 包含博弈双方,求最优策略
  2. 满足 1   v s   1 1~vs~1 1 vs 1 ,不能一对多,比如每匹马只能比一场,每块饼干只能给一个孩子

2、摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [ 1 , 7 , 4 , 9 , 2 , 5 ] [1,7,4,9,2,5] [1,7,4,9,2,5] 是一个摆动序列,因为差值 ( 6 , − 3 , 5 , − 7 , 3 ) (6,-3,5,-7,3) (6,3,5,7,3) 是正负交替出现的。相反, [ 1 , 4 , 7 , 2 , 5 ] [1,4,7,2,5] [1,4,7,2,5] [ 1 , 7 , 4 , 5 , 5 ] [1,7,4,5,5] [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。


分析

要求: 最长摆动子序列
约束条件: 连续数字之间的差严格地在正数和负数之间交替

根据约束条件可知,摆动序列中,除了边缘两个值以外,中间所有值必须满足下列条件之一:

  1. 大于左右相邻两个值
  2. 小于左右相邻两个值

即:中间所有值为序列的极值点
取下图中所有红色点可以构成最长摆动序列
在这里插入图片描述
P y t h o n 3 Python3 Python3 代码如下:

class Solution:
    def wiggleMaxLength(self, nums: List[int]) -> int:
        n = len(nums)
        if n < 2:
            return n

        count, flag = 1, 0
        for i in range(1, n):
            if nums[i] > nums[i-1] and flag != 1:
                count += 1
                flag = 1
            elif nums[i] < nums[i-1] and flag != -1:
                count += 1
                flag = -1
        return count

3、移除 K 个数字

给定一个以字符串表示的非负整数 n u m num num,移除这个数中的 k k k 位数字,使得剩下的数字最小。

注意:
n u m num num 的长度小于 10002 10002 10002 ≥ k ≥ k k
n u m num num 不会包含任何前导零。


分析

要求: 移除 K 位后剩下数字最小
注意: 如果移除 K 位后剩余数字的开头为 ′ 0 ′ &#x27;0&#x27; 0 则要将它们去除。

由于要求剩下数字最小,优先考虑移除高位较大的数字。逆向思考这个问题,移除 K 个数字等价于寻找 n-K 个数字组成的子序列最小。即,每一位找到该位置可能的最小。举例如下:

num = ‘125413’
K = 3
通过分析发现,去除 3 位后剩余 3 位,要寻找每一位可能的最小(优先满足高位最小)
第一位:由于可以删除 K 个,所以第一位可以选择前 4 位里最小的数为 1(位置0)K 仍然为 3
第二位:从位置 1 开始(因为上一个数选了位置 0),最多还可以删除 K 个,选择 1—4 位中最小的数,即 2(位置 1)K 仍然为 3
第三位:从位置 2 开始,最多删除 K 个,选择 2—5 位中最小的数 1 (位置 4)由于删除了 1 前面的 5 和 4 ,所以 K 修正为 1。
最后:去除后面的 K 位,最后得到 ‘121’,删除了‘543’

通过以上分析不难发现,这个题目和 滑动窗口的最大值 有异曲同工之妙,区别在于这里的窗口大小 K 是变化的,而且窗口不是逐个滑动的,而是根据上一个窗口最优解的位置来确定下一窗口位置,下一窗口的大小由之前删除的元素数量和初始 K 值共同决定。

因此,我们使用栈来实现这一算法。

  1. 将数字从头开始逐个放入栈中。如果当前值比栈顶元素小,并且 K > 0(说明还可以删除)
    1. 栈顶元素出栈(删除),修正 K 值,直到栈空,或者 K 为 0,或者当前值不大于栈顶元素为止
  2. 若以上过程结束后 K 仍然大于 0,则将栈顶的 K 个元素出栈。
  3. 验证栈底元素是否为 ‘0’,去除开始所有的 ‘0’。
class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        stack = []
        n = len(num)
        if n <= k:
            stack.append('0')
        else:
            for i in range(n):
                # 栈顶数字大于当前数字 k>0 出栈
                while stack and k > 0 and stack[-1] > num[i]:
                    stack.pop()
                    k -= 1
                stack.append(num[i])
            if k > 0:
                stack = stack[:-k]
        while stack and stack[0] == "0":  # 移除0
            stack.pop(0)
        res = ''.join(stack)
        return res if res else '0'   

4、跳跃游戏

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

思路:

  1. 从第 i i i 个位置,最远可以跳到 n u m s [ i ] + i nums[i]+i nums[i]+i
  2. 逐个位置遍历,始终记录当前所能到达的最远位置 r e a c h reach reach
  3. 当前位置超过所能到达的最远位置时,返回 F a l s e False False
  4. 所能到达的最远位置 r e a c h reach reach 超过最右边位置时,返回 T r u e True True

代码

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        reach = 0
        n = len(nums)
        for i in range(n):
            if i > reach or reach > n  :
                break
            if i + nums[i] > reach:
                reach = i + nums[i]
        return reach >= n - 1

5、跳跃游戏 Ⅱ

class Solution:
    def jump(self, nums: List[int]) -> int:
        
        n = len(nums)
        if n == 1:
            return 0
        
        reach = 0
        next_reach = nums[0]
        step = 0
        for i in range(n):
            next_reach = max(i + nums[i], next_reach)
            if next_reach >= n - 1:
                return step + 1
            if i == reach:
                step += 1
                reach = next_reach
        
        return step

6、射击气球

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

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

思路:

  1. 按气球区间的右端点进行从小到大排序,取当前右端点为射穿第一个气球的箭所能到达的最右的位置。
  2. 依次遍历每个气球的区间,若当前气球的左端点在上一气球的右端点之前(两个气球有重叠),则不需要新的箭,否则需要新的箭射穿当前气球。
class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        n = len(points)
        if n < 2:
            return n
         
        # 按照区间的末尾端点排序 
        points.sort(key = lambda x: x[1])
        res = 1
        # 最远距离:使用当前这只箭能引爆气球的最远距离
        reach = points[0][1]
        
        for i in range(1, n):
            if points[i][0] > reach:
                reach = points[i][1]
                res += 1
        return res

7、最优加油方法

在一条环路上有 N N N 个加油站,其中第 i i i 个加油站有汽油 g a s [ i ] gas[i] gas[i] 升。

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

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 − 1 -1 1

说明:

  1. 如果题目有解,该答案即为唯一答案。
  2. 输入数组均为非空数组,且长度相同。
  3. 输入数组中的元素均为非负数。

思路 1:

检查每个加油站:

  1. 以该加油站为起点
  2. 模拟环路行驶,若到达每一站的油量 ≥ 0 \ge0 0 则可以环路行驶。

该方法时间复杂度为 O ( n 2 ) O(n^2) O(n2)

思路 2:

  1. 初始化 t o t a l _ t a n k total\_tank total_tank c u r r _ t a n k curr\_tank curr_tank 0 0 0 ,并且选择 0 0 0 号加油站为起点。

  2. 遍历所有的加油站:

    • 每一步中,都通过加上 g a s [ i ] gas[i] gas[i] 和减去 c o s t [ i ] cost[i] cost[i] 来更新 t o t a l t a n k total_tank totaltank c u r r _ t a n k curr\_tank curr_tank

    • 如果在 i + 1 i + 1 i+1 号加油站, c u r r t a n k &lt; 0 curr_tank &lt; 0 currtank<0 ,将 i + 1 i + 1 i+1 号加油站作为新的起点,同时重置 c u r r _ t a n k = 0 curr\_tank = 0 curr_tank=0 ,让油箱也清空。

  3. 如果 t o t a l _ t a n k &lt; 0 total\_tank &lt; 0 total_tank<0 ,返回 − 1 -1 1 ,否则返回 s t a r t i n g   s t a t i o n starting~station starting station

时间复杂度为 O ( n ) O(n) O(n)

class Solution:
    def canCompleteCircuit(self, gas, cost):
        """
        :type gas: List[int]
        :type cost: List[int]
        :rtype: int
        """
        n = len(gas)
        
        total_tank, curr_tank = 0, 0
        start = 0
        for i in range(n):
            total_tank += gas[i] - cost[i]
            curr_tank += gas[i] - cost[i]
            # If one couldn't get here
            if curr_tank < 0:
                # Pick up the next station as the starting one.
                start = i + 1
                # Start with an empty tank.
                curr_tank = 0
        
        return start if total_tank >= 0 else -1
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值