代码随想录算法训练营Day 35 || 435. 无重叠区间、763.划分字母区间、56. 合并区间

435. 无重叠区间

力扣题目链接(opens new window)

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

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

这个问题的关键是如何在保留尽可能多的区间的同时,消除所有的重叠。解决这个问题的一个有效策略是使用贪心算法。以下是解题的基本思路和步骤:

  1. 排序:首先,将所有区间按照结束时间进行排序。这一步是非常关键的,因为你希望尽可能地让更多的区间能够适应,这就需要关注区间的结束时间,给其他的区间留出更多的空间。例如,如果有区间 [1,3] 和 [2,4],保留 [1,3] 会更有意义,因为它结束得更早,留给后面的区间更多的空间。

  2. 寻找重叠:接下来,你需要检查有哪些区间是重叠的。从排序后的第一个区间开始,检查每个区间是否与前一个区间重叠。如果当前区间的起始时间小于前一个区间的结束时间,那么它们是重叠的。

  3. 移除重叠:每当你找到重叠的区间时,都需要进行一个决定:保留哪一个区间?根据我们的贪心策略,我们应该保留结束时间更早的那个区间,因为这样可以为后续的区间留出更多的空间。因此,每当我们遇到重叠时,我们选择结束时间更晚的区间进行移除。

  4. 计数移除的区间:为了知道需要移除多少个区间,我们需要对每一次移除进行计数。这样,当我们遍历完所有的区间后,就知道了为了避免重叠需要移除的区间总数。

  5. 结果:遍历并比较所有区间之后,你就得到了需要移除的最少区间数以保证剩余区间互不重叠。


class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        # 如果intervals为空,直接返回0
        if not intervals:
            return 0
        
        # 根据区间的结束时间对所有区间进行排序
        intervals.sort(key=lambda x: x[1])
        
        # 初始化已选择的区间的结束时间为负无穷大
        end = float('-inf')
        
        # 初始化移除的区间数量为0
        removed = 0
        
        # 遍历所有区间
        for interval in intervals:
            # 如果当前区间的开始时间大于已选择的区间的结束时间
            # 则选择这个区间,更新end为当前区间的结束时间
            if interval[0] >= end:
                end = interval[1]
            else:
                # 否则,需要移除这个区间,增加移除的区间数量
                removed += 1
        
        # 返回移除的区间数量
        return removed

以示例1为例,逐步解释代码的运行过程。

示例1:

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

1. 排序: 首先,我们对区间按照结束点进行排序:

排序后: intervals = [[1,2], [1,3], [2,3], [3,4]]

2. 初始化变量:

removed = 0 (记录移除的区间数量) end = -∞ (记录当前已确定的无重叠区间的结束点)

3. 遍历排序后的区间:

  • 第1个区间 [1,2]:

    • 因为 1 (区间开始点) >= -∞ (end), 所以这个区间被接受。
    • 更新 end 为 2 (end 的新值为当前区间的结束点)。
  • 第2个区间 [1,3]:

    • 因为 1 (区间开始点) < 2 (end), 所以这个区间与前一个区间重叠。
    • 我们增加 removed 的值为1(表示移除一个区间)。
  • 第3个区间 [2,3]:

    • 因为 2 (区间开始点) >= 2 (end), 所以这个区间被接受。
    • 更新 end 为 3 (end 的新值为当前区间的结束点)。
  • 第4个区间 [3,4]:

    • 因为 3 (区间开始点) >= 3 (end), 所以这个区间被接受。
    • 更新 end 为 4 (end 的新值为当前区间的结束点)。

4. 结果: 遍历结束后, removed 的值为1, 表示我们需要移除1个区间来确保剩下的区间没有重叠。

输出: 1

763.划分字母区间

力扣题目链接

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

示例:

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

提示:

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

我们使用一个具体的例子来详细解释代码的运行过程。假设我们有字符串 s = "abacccd"

1. 记录每个字符的最后出现位置

首先,我们创建一个字典 last_occurrence 来记录每个字符在字符串中的最后出现位置。

s = "abacccd" last_occurrence = {'a': 2, 'b': 1, 'c': 6, 'd': 7}

2. 初始化变量

接下来,我们初始化片段的开始和结束位置,以及结果列表。

start = 0 end = 0 result = []

3. 开始遍历字符串

  • idx=0 时, char='a':

    • end = max(0, 2) = 2,因为字符 'a' 的最后出现位置是 2。
  • idx=1 时, char='b':

    • end = max(2, 1) = 2,因为字符 'b' 的最后出现位置是 1,但我们已经有一个更大的结束位置。
  • idx=2 时, char='a':

    • end 不变,仍然是2。
    • 此时 idx 等于 end,这意味着从当前的 start(即0)到 end(即2)是一个片段。因此我们记录这个片段的长度为 3 (因为 2 - 0 + 1 = 3) 并添加到结果列表。
    • 更新 start 为下一个索引,即 3
  • idx=3 时, char='c':当 idx=3 时, char='c'

    • end = max(2, 6) = 6,因为字符 'c' 的最后出现位置是 6。
  • 以此类推,直到遍历完整个字符串。在 idx=6idx=7 时,我们会再次找到另一个片段并将其长度添加到结果列表。

4. 最终结果

result 的值将为 [3, 5],这意味着我们可以将字符串划分为 "aba" 和 "cccd" 这两个片段。

from typing import List

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        # 记录每个字符的最后出现位置
        last_occurrence = {char: idx for idx, char in enumerate(s)}
        
        start, end = 0, 0
        result = []
        
        for idx, char in enumerate(s):
            # 更新当前片段的结束位置
            end = max(end, last_occurrence[char])
            
            # 当遍历到当前片段的结束位置,记录片段长度并开始新的片段
            if idx == end:
                result.append(end - start + 1)
                start = idx + 1
                
        return result

代码步骤:

  1. 创建字典记录每个字符的最后出现位置:

last_occurrence = {char: idx for idx, char in enumerate(s)}

此字典为我们提供了每个字符的最后出现位置的快速查找。

  1. 初始化startend0:

    • start是当前片段的开始位置。
    • end是当前片段的预期结束位置。
  2. 遍历字符串s的每个字符和其索引:

for idx, char in enumerate(s):

  1. 更新当前片段的结束位置:
    • 使用当前字符的最后出现位置和当前end值中的较大者来更新end

end = max(end, last_occurrence[char])

  1. 检查是否达到片段的结束位置:
    • idx(当前遍历的位置)等于end时,这意味着从startend是一个有效的片段,因为我们确保了这个片段中的所有字符都不会在之后出现。

if idx == end: result.append(end - start + 1) start = idx + 1

  • 我们将该片段的长度添加到结果列表result中。
  • 然后,我们将start设置为idx + 1,以开始新的片段。
  • 最后返回结果列表result

56. 合并区间

力扣题目链接(opens new window)

给出一个区间的集合,请合并所有重叠的区间。

示例 1:

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

示例 2:

  • 输入: intervals = [[1,4],[4,5]]
  • 输出: [[1,5]]
  • 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
  • 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。


要解决这个问题,我们可以遵循以下步骤:

  1. 如果区间列表为空或只包含一个区间,我们可以直接返回这个列表,因为没有什么可以合并的。
  2. 将区间列表按照每个区间的起始位置进行排序。
  3. 初始化一个空的列表 merged 来存储合并后的区间。
  4. 遍历排序后的区间列表,对于每个区间 interval
    • 如果 merged 为空或者 merged 中最后一个区间的结束位置小于当前 interval 的起始位置,说明当前区间与 merged 中的所有区间都不重叠,可以直接将 interval 添加到 merged
    • 否则,说明当前区间与 merged 中最后一个区间有重叠,我们需要更新 merged 中最后一个区间的结束位置为当前区间的结束位置和 merged 中最后一个区间的结束位置的较大值。
  5. 遍历完成后,merged 中存储的就是合并后的区间列表。
from typing import List

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        if not intervals:
            return []
        
        # Step 2: Sort the intervals based on the start of each interval
        intervals.sort(key=lambda x: x[0])
        merged = [intervals[0]]
        
        # Step 3 & 4: Iterate through the intervals
        for interval in intervals[1:]:
            # If the current interval does not overlap with the previous, add it to merged
            if merged[-1][1] < interval[0]:
                merged.append(interval)
            # If it does overlap, merge the current interval with the previous one
            else:
                merged[-1][1] = max(merged[-1][1], interval[1])
                
        # Step 5: Return the merged intervals
        return merged

让我们以示例 [[1,3],[2,6],[8,10],[15,18]] 来解释这段代码的逻辑。

  1. 首先,我们创建一个 Solution 类的实例,并调用 merge 方法。

    sol = Solution() result = sol.merge([[1,3],[2,6],[8,10],[15,18]])

  2. merge 方法中,首先检查输入的 intervals 是否为空。如果为空,直接返回空列表。否则,继续执行。

  3. 接下来,对 intervals 按照每个区间的起始位置进行排序。排序后的 intervals 将变为 [[1,3],[2,6],[8,10],[15,18]]

  4. 初始化一个列表 merged 并将排序后的第一个区间 [1,3] 加入其中。

  5. 现在,遍历剩余的区间:

    • 首先查看 [2,6]merged 中最后一个区间是 [1,3]。因为 [1,3] 的结束位置 3 大于等于 [2,6] 的起始位置 2,我们知道这两个区间是重叠的。所以我们将 merged 中最后一个区间的结束位置更新为 max(3,6),即 6。现在,merged 变为 [[1,6]]
    • 然后查看 [8,10]merged 中最后一个区间是 [1,6]。因为 [1,6] 的结束位置 6 小于 [8,10] 的起始位置 8,我们知道这两个区间不重叠。所以直接将 [8,10] 加入到 merged 中。现在,merged 变为 [[1,6],[8,10]]
    • 最后查看 [15,18],同理,直接加入到 merged 中,因为它不与 [8,10] 重叠。现在,merged 变为 [[1,6],[8,10],[15,18]]
  6. 遍历完成后,返回 merged,即 [[1,6],[8,10],[15,18]]。这就是合并后的区间列表。

你可能感兴趣的:(leetcode,leetcode)