leetcode-贪心

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、贪心算法

贪心算法是对完成一件事情的方法的描述,贪心算法每一次都做出当前看起来最好的选择,而不用考虑其它可能的选择。

  1. 贪心算法与回溯算法、动态规划的区别
  • 「回溯算法」需要记录每一个步骤、每一个选择,用于回答所有具体解的问题;
  • 「动态规划」需要记录的是每一个步骤、所有选择的汇总值(最大、最小或者计数);
  • 「贪心算法」由于适用的问题,每一个步骤只有一种选择,一般而言只需要记录与当前步骤相关的变量的值。
  1. 可以使用「贪心算法」的问题需要满足的条件
  • 最优子结构:规模较大的问题的解由规模较小的子问题的解组成,区别于「动态规划」,可以使用「贪心算法」的问题「规模较大的问题的解」只由其中一个「规模较小的子问题的解」决定;
  • 无后效性:后面阶段的求解不会修改前面阶段已经计算好的结果;
  • 贪心选择性质:从局部最优解可以得到全局最优解。(如从10张钞票中选5次,使得到总额最大,那么我每次选取最大(局部最优),最后总额也是最大的),贪心其实和常识没啥区别(模拟)

对「最优子结构」和「无后效性」的理解同「动态规划」,「贪心选择性质」是「贪心算法」最需要关注的内容。

总结:

  • 「贪心算法」总是做出在当前看来最好的选择就可以完成任务;
  • 解决「贪心算法」几乎没有套路,到底如何贪心,贪什么与我们要解决的问题密切相关。因此刚开始学习「贪心算法」的时候需要学习和模仿,然后才有直觉,猜测一个问题可能需要使用「贪心算法」,进而尝试证明,学会证明。

二、题库

1.简

455. 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 1:

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

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        # 思路:排序,先从胃口最大的开始给,如果没有满足的饼干,则跳入下一个小孩子
        i = len(g) - 1
        j = len(s) - 1
        cnt = 0
        g.sort()
        s.sort()
        while min(i,j) >= 0:
            if s[j] >= g[i]:
                cnt += 1
                j -= 1
            i -= 1
        return cnt

复杂度分析

  • 时间复杂度:O(mlogm+nlogn),其中 m 和 n 分别是数组 g 和 s 的长度。对两个数组排序的时间复杂度是 O(mlogm+nlogn),遍历数组的时间复杂度是 O(m+n),因此总时间复杂度是 O(mlogm+nlogn)。

  • 空间复杂度:O(logm+logn),其中 m 和 n 分别是数组 g 和 s 的长度。空间复杂度主要是排序的额外空间开销。

860. 柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

示例 1:

输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

class Solution:
    def lemonadeChange(self, bills: List[int]) -> bool:
        # 贪心,每一次选择一次,就和模拟相似
        five=0
        ten=0
        for i in bills:
            if i==5:
                five+=1
            elif i==10:
                five-=1
                ten+=1
            else:
                if ten>=1:
                    ten-=1
                    five-=1
                else:
                    five-=3
            if five<0 or ten<0:
                return False
        return True

复杂度分析

  • 时间复杂度:O(n),

  • 空间复杂度:O(1)

2.中

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

注意:

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

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

输出: 1

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

思路:
先原区间(右节点)进行排序,我们可以不断地寻找右端点在首个区间右端点左侧的新区间,将首个区间替换成该区间。那么当我们无法替换时,首个区间就是所有可以选择的区间中右端点最小的那个区间。因此我们将所有区间按照右端点从小到大进行排序,那么排完序之后的首个区间,就是我们选择的首个区间。就是每一次找到符合的,最后进行总数相减。

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        # 贪心,每一次选择最优
        intervals.sort(key=lambda x: x[1])
        right=intervals[0][1]
        ans=1
        for i in range(1,len(intervals)):
            if intervals[i][0]>=right:
                ans+=1
                right=intervals[i][1]
        return len(intervals)-ans

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是区间的数量。我们需要 O(nlogn) 的时间对所有的区间按照右端点进行升序排序,并且需要 O(n) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O(nlogn)。
  • 空间复杂度:O(logn),即为排序需要使用的栈空间。
class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        # 动态规划:对itervals进行排序,对每一个区间进行遍历,进行统计其当前区间作为最后区间满足的区间个数:
        # 最优子结构:  dp[i]=max(dp[j] if itervals[i][0]>=itervals[j][1])+1
        # 初始条件:dp[0]=1
        if not intervals:
            return 0
        intervals.sort()
        n = len(intervals)
        f = [1]
        for i in range(1, n):
            f.append(max((f[j] for j in range(i) if intervals[j][1] <= intervals[i][0]), default=0) + 1)
        return n - max(f)

复杂度分析

  • 时间复杂度:O(n^2),

  • 空间复杂度:O(logn),即为排序需要使用的栈空间。

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

一支弓箭可以沿着 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 射爆另外两个气球

class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        # 思路:贪心,就是寻找无重叠区间的个数,每个每个最优解进行求解,
        points.sort(key=lambda x :x[1])
        right=points[0][1]
        ans=1
        for i in range(1,len(points)):
            if points[i][0]>right:
                ans+=1
                right=points[i][1]
        return ans

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是区间的数量。我们需要 O(nlogn) 的时间对所有的区间按照右端点进行升序排序,并且需要 O(n) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O(nlogn)。
  • 空间复杂度:O(logn),即为排序需要使用的栈空间。

1536. 排布二进制网格的最少交换次数
给你一个 n x n 的二进制网格 grid,每一次操作中,你可以选择网格的 相邻两行 进行交换。

一个符合要求的网格需要满足主对角线以上的格子全部都是 0 。

请你返回使网格满足要求的最少操作次数,如果无法使网格符合要求,请你返回 -1 。

主对角线指的是从 (1, 1) 到 (n, n) 的这些格子。

示例 1:

输入:grid = [[0,0,1],[1,1,0],[1,0,0]]
输出:3

class Solution:
    def minSwaps(self, grid: List[List[int]]) -> int:
        # 1. 思路:贪心 时间复杂度O(n^2) 空间 o(n)
        pos=[0]*len(grid)
        n=len(grid)
        for i in range(n):
            for j in range(n-1,-1,-1):
                if grid[i][j]==1:
                    pos[i]=j
                    break
        cnt=0
        for i in range(n):# 贪心 对每行都保证,正确
            exchange=-1
            if pos[i]>i:
                for j in range(i,len(pos)):
                    if pos[j]<=i:# 则找到应该交换的位置了
                        exchange=j
                        cnt+=exchange-i
                        break
                if exchange==-1:
                    return -1
                for j in range(exchange,i,-1):          # 开始进行两两交换
                    pos[j],pos[j-1]=pos[j-1],pos[j]
            else:
                continue
        return  cnt

56. 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

示例 1:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        # 和前面两个不同,前两个无论是对哪一个位置排序都行,但是合并区间,对第二个位置排序,解决不了,
        intervals.sort(key=lambda x :x[0])
        ans=[intervals[0]]
        for i in range(1,len(intervals)):
            if intervals[i][0]<=ans[-1][1]:
                ans[-1][0]=min(ans[-1][0],intervals[i][0])
                ans[-1][1]=max(ans[-1][1],intervals[i][1])
            else:
                ans.append(intervals[i])
        return ans

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是区间的数量。我们需要 O(nlogn) 的时间对所有的区间按照右端点进行升序排序,并且需要 O(n) 的时间进行遍历。由于前者在渐进意义下大于后者,因此总时间复杂度为 O(nlogn)。
  • 空间复杂度:O(logn),即为排序需要使用的栈空间。

55. 跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

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

判断你是否能够到达最后一个下标。

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        # 思路1:贪心,每一步,都是当前走过的最优步数, i+nums[i] i表示我已走的步数,nums[i]表示当前第i步我可以跳跃的的最大长度,因而相加就等价于,整体跳跃的最大长度
        n, rightmost = len(nums), 0
        len_nums=len(nums)
        for i in range(len_nums):
            if i<=rightmost:# 只有当,rightmost>=i 则表明能走到第i步,才可以计算。
                rightmost=max(rightmost,i+nums[i])
                print(i,rightmost)
                if rightmost>=len_nums-1:
                    return True
        return False

复杂度分析

  • 时间复杂度:O(n),
  • 空间复杂度:O(1),

45. 跳跃游戏 II
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。

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

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

假设你总是可以到达数组的最后一个位置。

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
在这里插入图片描述
贪心的思路其实就是,如我在索引为0这个点,开始走,其走的步数是二,那么有一个特点,他的下一步是有两个值为3,1,则进行计算3和1这两个点,能到达最远的距离,即3+1,和2+1 进行比较,

class Solution:
    def jump(self, nums: List[int]) -> int:
        # 思路:贪心 i+nums[i]指的是当前能走的最远距离
        nums_len=len(nums)
        positon=0
        step=0
        end=0
        for i in range(nums_len-1):
            if positon>=i:
                positon=max(positon,i+nums[i])      # 
                if i==end:
                    end=positon
                    step+=1
        return step

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组长度。

  • 空间复杂度:O(1)。

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

示例:

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

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        #  贪心加双指针   将每一个字母最后一次出现的索引记性存储,其次遍历整个数组,end表示走到的最长路径,当遍历的路径和走路的路径最长相等时,表明当前已经走到可以分块了。  时间复杂度为o(n) 空间复杂度为  o(26)
        last=[0]*26
        for index in range(len(s)):
            last[ord(s[index])-ord('a')]=index
        partition=[]
        start=end=0
        for index,val in enumerate(s):
            end=max(end,last[ord(val)-ord('a')])
            if index==end:
                partition.append(end-start+1)
                start=end+1
        return partition



三、基础与练习

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

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

进阶:

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

示例 1:

输入:s = “abc”, t = “ahbgdc”
输出:true

class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
    	# 思路:贪心,每次匹配到s,则s索引往下移
        if len(s)==0:
            return True
        s_index=0
        flag=False
        for i in t:
            if i==s[s_index]:
                s_index+=1
            if s_index==len(s):
                flag=True
                break
        return flag

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组长度。

  • 空间复杂度:O(1)。

561. 数组拆分 I
给定长度为 2n 的整数数组 nums ,你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) ,使得从 1 到 n 的 min(ai, bi) 总和最大。

返回该 最大总和 。

示例 1:

输入:nums = [1,4,3,2]
输出:4
解释:所有可能的分法(忽略元素顺序)为:

  1. (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3
  2. (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3
  3. (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4
    所以最大总和为 4
class Solution:
    def arrayPairSum(self, nums: List[int]) -> int:
        # 排序,直接取奇数位的值
        nums.sort()
        ans=0
        for i in range(len(nums)):
            if i%2==0:
                ans+=nums[i]
        return ans

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是数组长度。

  • 空间复杂度:O(logn)。排序需要栈的空间

1710. 卡车上的最大单元数
请你将一些箱子装在 一辆卡车 上。给你一个二维数组 boxTypes ,其中 boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi] :

numberOfBoxesi 是类型 i 的箱子的数量。
numberOfUnitsPerBoxi 是类型 i 每个箱子可以装载的单元数量。
整数 truckSize 表示卡车上可以装载 箱子 的 最大数量 。只要箱子数量不超过 truckSize ,你就可以选择任意箱子装到卡车上。

返回卡车可以装载 单元 的 最大 总数。

示例 1:

输入:boxTypes = [[1,3],[2,2],[3,1]], truckSize = 4
输出:8
解释:箱子的情况如下:

  • 1 个第一类的箱子,里面含 3 个单元。
  • 2 个第二类的箱子,每个里面含 2 个单元。
  • 3 个第三类的箱子,每个里面含 1 个单元。
    可以选择第一类和第二类的所有箱子,以及第三类的一个箱子。
    单元总数 = (1 * 3) + (2 * 2) + (1 * 1) =
class Solution:
    def maximumUnits(self, boxTypes: List[List[int]], truckSize: int) -> int:
        # 贪心:排序,每一次获取单元数量最多的箱子尽量装满
        boxTypes.sort(key=lambda x:x[1],reverse=True)
        ans=0
        for i in boxTypes:
            if truckSize-i[0]>=0:
                ans+=i[0]*i[1]
                truckSize-=i[0]
            else:
                ans+=truckSize*i[1]
                return ans
        return ans

复杂度分析

  • 时间复杂度:O(nlogn),其中 n 是数组长度。

  • 空间复杂度:O(logn)。排序需要栈的空间

1217. 玩筹码
数轴上放置了一些筹码,每个筹码的位置存在数组 chips 当中。

你可以对 任何筹码 执行下面两种操作之一(不限操作次数,0 次也可以):

将第 i 个筹码向左或者右移动 2 个单位,代价为 0。
将第 i 个筹码向左或者右移动 1 个单位,代价为 1。
最开始的时候,同一位置上也可能放着两个或者更多的筹码。

返回将所有筹码移动到同一位置(任意位置)上所需要的最小代价。

示例 1:

输入:chips = [1,2,3]
输出:1
解释:第二个筹码移动到位置三的代价是 1,第一个筹码移动到位置三的代价是 0,总代价为 1。

class Solution:
    def minCostToMoveChips(self, position: List[int]) -> int:
        # 统计奇偶个数,返回数量最小的
        even=0
        odd=0
        for i in position:
            if i%2:
                odd+=1
            else:
                even+=1
        return min(odd,even)

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组长度。

  • 空间复杂度:O(1)。

122. 买卖股票的最佳时机 II
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。

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

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

示例 1:

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

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 思路:贪心,由题目可以很容易得出只要当前价格比上一次价格有提升,则购买和卖出
        price=0
        for i in range(1,len(prices)):
            temp=prices[i]-prices[i-1]
            if temp>0:
                price+=temp
        return price

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组长度。

  • 空间复杂度:O(1)。

2038. 如果相邻两个颜色均相同则删除当前颜色
总共有 n 个颜色片段排成一列,每个颜色片段要么是 ‘A’ 要么是 ‘B’ 。给你一个长度为 n 的字符串 colors ,其中 colors[i] 表示第 i 个颜色片段的颜色。

Alice 和 Bob 在玩一个游戏,他们 轮流 从这个字符串中删除颜色。Alice 先手 。

如果一个颜色片段为 ‘A’ 且 相邻两个颜色 都是颜色 ‘A’ ,那么 Alice 可以删除该颜色片段。Alice 不可以 删除任何颜色 ‘B’ 片段。
如果一个颜色片段为 ‘B’ 且 相邻两个颜色 都是颜色 ‘B’ ,那么 Bob 可以删除该颜色片段。Bob 不可以 删除任何颜色 ‘A’ 片段。
Alice 和 Bob 不能 从字符串两端删除颜色片段。
如果其中一人无法继续操作,则该玩家 输 掉游戏且另一玩家 获胜 。
假设 Alice 和 Bob 都采用最优策略,如果 Alice 获胜,请返回 true,否则 Bob 获胜,返回 false。

示例 1:

输入:colors = “AAABABB”
输出:true
解释:
AAABABB -> AABABB
Alice 先操作。
她删除从左数第二个 ‘A’ ,这也是唯一一个相邻颜色片段都是 ‘A’ 的 ‘A’ 。

现在轮到 Bob 操作。
Bob 无法执行任何操作,因为没有相邻位置都是 ‘B’ 的颜色片段 ‘B’ 。
因此,Alice 获胜,返回 true 。

class Solution:
    def winnerOfGame(self, colors: str) -> bool:
    # 贪心,时间o(n) 空间o(1)
        dict_colors=[0,0]
        curr,cnt='C',0
        for c in colors:
            if c!=curr:
                cnt=1
                curr=c
            else:
                cnt+=1
                if cnt>=3:
                    dict_colors[ord(curr)-ord('A')]+=1
        if dict_colors[0]>dict_colors[1]:
            return True
        return False
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值