代码随想录训练营 Day30打卡 贪心算法 part04
一、 力扣452. 用最少数量的箭引爆气球
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 :
输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。
局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。
为了让气球尽可能的重叠,需要对数组进行排序。
如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序)
可以看出首先第一组重叠气球,一定是需要一个箭,气球3 的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。
版本一
初始化箭的数量为1,因为至少需要一支箭来射穿第一个气球。
从第二个气球开始遍历,检查每个气球的起始点与前一个气球的终点。
如果当前气球的起点大于前一个气球的终点,说明这两个气球不重叠,需要增加一支箭。
如果重叠,则更新当前气球的右边界为两者的较小值,以便下一次判断是否可以用同一支箭射穿。
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
# 如果没有气球,返回0
if len(points) == 0:
return 0
# 将气球按起始点升序排序
points.sort(key=lambda x: x[0])
# 初始化箭的数量为1,默认射出第一箭
result = 1
# 遍历排序后的气球列表,从第二个气球开始
for i in range(1, len(points)):
# 如果当前气球的起点大于前一个气球的终点,说明这两个气球不重叠,需要一支新的箭
if points[i][0] > points[i - 1][1]: # 注意这里用的是 '>' 而不是 '>='
result += 1 # 增加箭的数量
else:
# 如果气球重叠,更新当前的最小右边界为两者中的较小值
# 这样保证在重叠的气球中,一箭可以射穿所有重叠的气球
points[i][1] = min(points[i - 1][1], points[i][1])
# 返回总共需要的箭的数量
return result
版本二
初始化最小左边界sl和最小右边界sr为第一个气球的位置。
初始化箭的数量为1,因为至少需要一支箭来射穿第一个气球。
遍历所有气球,对于每个气球,检查其起点与当前记录的最小右边界sr。
如果当前气球的起点大于sr,说明这些气球不重叠,需要一支新的箭,并更新最小左边界和最小右边界为当前气球的位置。
如果当前气球和之前的气球重叠,则更新最小左边界和最小右边界,以确保能用同一支箭射穿重叠的气球。
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
# 将气球按起始点升序排序
points.sort(key=lambda x: x[0])
# 初始化最小左边界和最小右边界为第一个气球的位置
sl, sr = points[0][0], points[0][1]
# 初始化箭的数量为1
count = 1
# 遍历排序后的气球列表
for i in points:
# 如果当前气球的起点大于当前记录的最小右边界,说明需要一支新的箭
if i[0] > sr:
count += 1 # 增加箭的数量
sl, sr = i[0], i[1] # 更新最小左边界和最小右边界为当前气球的位置
else:
# 如果当前气球和之前的气球重叠,更新最小左边界和最小右边界
sl = max(sl, i[0]) # 更新最小左边界为较大的起点
sr = min(sr, i[1]) # 更新最小右边界为较小的终点
# 返回总共需要的箭的数量
return count
二、 力扣435. 无重叠区间
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的 最小数量 ,使剩余区间互不重叠 。
示例 :
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
版本一
- 检查输入:
如果intervals为空,直接返回0,因为没有区间需要处理。 - 排序:
将区间按照左边界x[0]进行升序排序。这样可以保证后续的区间从左到右处理,方便判断是否重叠。 - 遍历区间:
从第二个区间开始,检查当前区间的左边界是否小于前一个区间的右边界。
如果重叠,更新当前区间的右边界为两个区间右边界的较小值,以减少后续的重叠可能性,并增加重叠计数count。
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
if not intervals:
return 0 # 如果区间列表为空,返回0
intervals.sort(key=lambda x: x[0]) # 按照左边界升序排序
count = 0 # 记录重叠区间的数量
# 从第二个区间开始遍历
for i in range(1, len(intervals)):
if intervals[i][0] < intervals[i - 1][1]: # 当前区间的左边界小于前一个区间的右边界,说明重叠
# 如果重叠,更新当前区间的右边界为较小值,以减少后续的重叠可能性
intervals[i][1] = min(intervals[i - 1][1], intervals[i][1])
count += 1 # 记录一次重叠
return count # 返回需要移除的重叠区间的数量
版本二
- 检查输入:
如果intervals为空,直接返回0,因为没有区间需要处理。 - 排序:
将区间按照左边界x[0]进行升序排序,确保从左到右逐个处理区间。 - 初始化不重叠区间计数:
result初始值设为1,因为至少会有一个不重叠的区间(第一个区间)。 - 遍历区间:
从第二个区间开始,检查当前区间的左边界是否大于等于前一个区间的右边界。
如果不重叠,增加不重叠区间的计数result。
如果重叠,更新当前区间的右边界为两个区间右边界的较小值,以减少后续的重叠可能性。
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
if not intervals:
return 0 # 如果区间列表为空,返回0
intervals.sort(key=lambda x: x[0]) # 按照左边界升序排序
result = 1 # 记录不重叠区间的数量,初始化为1,因为至少有一个不重叠的区间
# 从第二个区间开始遍历
for i in range(1, len(intervals)):
if intervals[i][0] >= intervals[i - 1][1]: # 当前区间的左边界大于等于前一个区间的右边界,说明不重叠
result += 1 # 不重叠的区间数加1
else: # 如果重叠
# 更新当前区间的右边界为较小值,以减少后续的重叠可能性
intervals[i][1] = min(intervals[i - 1][1], intervals[i][1])
# 总区间数减去不重叠的区间数,就是需要移除的区间数
return len(intervals) - result
三、 力扣763. 划分字母区间
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
示例 :
输入:s = “ababcbacadefegdehijhklij”
输出:[9,7,8]
解释:
划分结果为 “ababcbaca”、“defegde”、“hijhklij” 。
每个字母最多出现在一个片段中。
像 “ababcbacadefegde”, “hijhklij” 这样的划分是错误的,因为划分的片段数较少。
在遍历的过程中相当于是要找每一个字母的边界,**如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。**此时前面出现过所有字母,最远也就到这个边界了。
可以分为如下两步:
- 统计每一个字符最后出现的位置
- 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
如图:
版本一
- 记录字符的最后出现位置:
使用字典last_occurrence记录每个字符在字符串中最后出现的位置。通过遍历字符串,将每个字符的最新位置存入字典。
- 初始化变量:
result用于存储最终划分出的每个区间的长度。
start和end分别表示当前区间的起始和结束位置。
- 遍历字符串进行区间划分:
在遍历字符串时,不断更新当前区间的结束位置end为当前字符的最后出现位置。
当当前位置i等于end时,意味着当前区间可以结束,将这个区间的长度添加到结果中,并更新起始位置start。
class Solution:
def partitionLabels(self, s: str) -> List[int]:
last_occurrence = {} # 存储每个字符最后出现的位置
for i, ch in enumerate(s):
last_occurrence[ch] = i # 记录字符ch在字符串s中的最后出现位置
result = [] # 存储每个区间的长度
start = 0 # 当前区间的起始位置
end = 0 # 当前区间的结束位置
for i, ch in enumerate(s):
# 更新当前区间的结束位置为字符ch最后出现的位置
end = max(end, last_occurrence[ch])
# 如果当前位置i是当前区间的结束位置,则完成一个区间的划分
if i == end:
result.append(end - start + 1) # 将区间的长度加入结果列表
start = i + 1 # 更新起始位置为下一个字符的位置
return result # 返回划分出的区间长度列表
版本二
- 统计每个字符的起始和结束位置:
countLabels函数通过遍历字符串,记录每个字母的起始和结束位置。使用长度为26的数组hash存储这些位置,hash[i][0]表示字母i的起始位置,hash[i][1]表示字母i的结束位置。
- 过滤有效的区间:
遍历hash数组,过滤掉未出现的字母,只保留有效的区间信息,存入hash_filter列表。
- 排序区间:
按照区间的左边界从小到大排序,为后续区间合并和划分做准备。
- 遍历区间进行划分:
初始化rightBoard为第一个区间的右边界,leftBoard为0。
遍历排序后的区间列表,若发现当前区间的左边界大于当前的最大右边界,说明可以划分出一个新的区间。更新左边界和右边界,并记录划分出的区间长度。
- 添加最后一个区间:
在循环结束后,别忘了添加最后一个区间的长度。
class Solution:
def countLabels(self, s):
# 初始化一个长度为26的区间列表,用于记录每个字母的起始和结束位置
hash = [[float('-inf'), float('-inf')] for _ in range(26)]
hash_filter = []
# 遍历字符串,记录每个字母的起始和结束位置
for i in range(len(s)):
# 如果当前字母第一次出现,记录它的起始位置
if hash[ord(s[i]) - ord('a')][0] == float('-inf'):
hash[ord(s[i]) - ord('a')][0] = i
# 更新当前字母的结束位置为最新位置
hash[ord(s[i]) - ord('a')][1] = i
# 过滤掉没有出现的字母,生成有效的区间列表
for i in range(len(hash)):
if hash[i][0] != float('-inf'):
hash_filter.append(hash[i])
return hash_filter # 返回有效的区间列表
def partitionLabels(self, s):
res = []
hash = self.countLabels(s) # 获取有效的区间列表
hash.sort(key=lambda x: x[0]) # 按左边界从小到大排序
rightBoard = hash[0][1] # 初始化最大右边界为第一个区间的右边界
leftBoard = 0 # 初始化左边界为0
for i in range(1, len(hash)):
# 如果当前区间的左边界大于当前的最大右边界,说明可以划分一个新的区间
if hash[i][0] > rightBoard:
res.append(rightBoard - leftBoard + 1) # 添加划分的区间长度到结果列表
leftBoard = hash[i][0] # 更新左边界为当前区间的左边界
# 更新当前的最大右边界
rightBoard = max(rightBoard, hash[i][1])
# 添加最后一个区间的长度到结果列表
res.append(rightBoard - leftBoard + 1)
return res # 返回划分出的区间长度列表