会议室 - 检查一个人是否能参加所有会议(C++)

2025年3月21日 | 阅读 11 分钟

引言

C++ 中的“会议室”问题旨在确定一个人是否能在不发生冲突的情况下参加所有安排的会议。每个会议都由一个时间间隔表示,包含开始和结束时间,目标是检查会议是否会以任何方式冲突。

假设您全天有几次会议,每次都有开始和结束时间。如果任何两次会议发生重叠,您将无法同时参加。挑战在于检查这些时间间隔并决定您是否可以参加所有会议而没有任何冲突。

关键概念:时间间隔重叠

会议时间间隔可以描述为 [开始, 结束],其中

  • 开始 是会议开始的时间。
  • 结束 是会议结束的时间(不包含,意味着下一个会议可以在前一个会议的精确结束时间开始)。

当且仅当一个会议在先前会议结束之前开始时,两个时间间隔才会重叠。

  • 给定两个时间间隔 [start1, end1] 和 [start2, end2],如果 start2 < end1,则发生重叠。

为避免冲突,我们必须确保没有两次会议发生重叠。

方法 1:朴素方法

实施

输出

 
No, the person cannot attend all meetings.   

说明

步骤 1:构建问题

第一步是认识到每次会议都有一个开始时间和结束时间。这些被视为时间间隔,每个时间间隔代表一次会议的安排。问题变成检查这些时间间隔中是否有任何重叠,这意味着一个人将无法参加两次重叠的会议。

步骤 2:对会议进行排序

为了更容易地检测重叠,会议是根据它们的开始时间排序的。排序有助于以一种确保每个会议都按顺序与下一个会议进行检查的方式来安排会议。这样,一旦排序,您只需要比较每个会议与其紧邻的会议,从而降低了复杂性。

步骤 3:检测重叠

一旦时间间隔按开始时间排序,下一步就是检查重叠。当一个会议在先前会议结束之前开始时,就会发生重叠。

例如

  • 假设您有两个会议:一个从下午 1 点到下午 3 点,另一个从下午 2 点到下午 4 点。第二个会议在下午 2 点开始,早于第一个会议在下午 3 点结束,这意味着这两个会议重叠,您无法同时参加。

步骤 4:遍历会议

排序后,您将逐一浏览会议列表

  • 从比较第二个会议的开始时间与第一个会议的结束时间开始。
  • 如果第二个会议的开始时间小于第一个会议的结束时间,则发现重叠,您可以立即得出结论,您无法参加所有会议。
  • 如果没有检测到重叠,请继续比较下一对会议,遍历列表。

步骤 5:返回结果

目标是遍历整个会议列表,比较连续的时间间隔。如果在任何时候发现重叠,函数将立即返回“false”,表示无法参加所有会议。如果您在不发现任何重叠的情况下遍历了整个列表,则函数返回“true”,确认可以参加所有会议。

复杂度分析

时间复杂度

对时间间隔进行排序

第一个重要的操作是按开始时间对会议时间间隔列表进行排序。排序通常需要 O(nlogn) 时间,其中 n 是时间间隔(或会议)的数量。这是时间复杂度中的主导因素,因为排序是过程中最耗时的部分。

检查重叠

排序后,下一步是遍历排序后的时间间隔以检查重叠。这需要对列表进行一次遍历,耗时 O(n)。每个时间间隔都与前一个进行比较,因此此步骤的复杂度是线性的。

由于排序操作占主导地位,因此总体时间复杂度为 O(nlogn)。

空间复杂度

原地排序

如果使用的排序算法是原地排序(例如 C++ 的 std::sort 函数),除了少量恒定量的额外内存外,排序不需要额外的空间。因此,排序步骤需要 O(1) 的额外空间。

输入存储

时间间隔本身存储在一个列表中,由于有 n 个时间间隔,因此需要 O(n) 的空间。

辅助空间

重叠检查部分算法仅使用几个变量来存储索引和比较时间间隔,这是恒定的空间,即 O(1)。

因此,由于输入时间间隔的存储,总体空间复杂度为 O(n)。除了输入存储之外,排序或检查不需要额外的空间。

方法 2:使用最小堆(优先队列)

此方法通过关注最早结束的会议来动态管理会议冲突,从而使我们能够确定是否可以参加新会议。

实施

输出

 
No, the person cannot attend all meetings.   

说明

步骤 1:理解问题

我们有几个会议,每个会议都由开始和结束时间定义。任务是弄清楚一个人是否可以参加所有这些会议而没有任何重叠。在此方法中,我们希望以一种可以快速检查新会议是否与先前安排的会议冲突的方式来动态管理会议时间。

步骤 2:按开始时间对会议进行排序

我们要做的第一件事是将会议按开始时间顺序排列。这样对会议进行排序可以确保每个会议只与后续的下一个会议进行比较。这一点很重要,因为如果两个会议之间存在任何冲突,当它们一个接一个地检查时,最容易发现。已排序的会议列表是后续步骤的基础。

步骤 3:使用最小堆跟踪最早结束时间

为了有效地管理会议的结束时间,使用最小堆(也称为优先队列)。最小堆是一种数据结构,它始终为您提供访问最小元素的权限。在这种情况下,是开始时间最早的会议。这一点很重要,因为如果您知道最早结束的会议,您可以快速确定下一个会议是否可以在它之后开始,或者是否存在重叠。

最初,第一个会议的结束时间被添加到最小堆中。这样,我们首先跟踪第一个会议何时结束。

步骤 4:处理每个会议

现在,对于后续的每个会议

  • 您将当前会议的开始时间与任何正在进行的会议的最早结束时间(最小堆顶部的那个)进行比较。
  • 如果当前会议在最早结束的会议之后或同时开始,则没有冲突,并且可以从堆中删除最早结束的会议。这表明较早的会议已结束,当前会议可以继续进行而不会发生任何重叠。
  • 之后,您将当前会议的结束时间添加到堆中,这表明该会议正在进行中,并可能与未来的会议发生重叠。

步骤 5:检查重叠

堆设计用于存储正在进行的会议的结束时间。如果堆变得太大或多个会议在时间上重叠,则意味着您将无法参加所有会议。具体来说,如果一个会议在最早的会议结束之前开始,就会造成冲突。

如果在处理完所有会议后,堆中只有一个会议,则表示所有会议都已处理完毕,没有任何冲突,并且您可以参加所有会议。

复杂度分析

时间复杂度

对会议进行排序

根据开始时间对会议列表进行排序是此方法中最耗时的操作。通常使用的排序算法(如 std::sort)的时间复杂度为 O(nlogn),其中 n 是会议的数量。

使用最小堆处理会议

  • 排序后,算法处理每个会议一次。对于每个会议,它执行两个主要操作:
    1. 将结束时间插入最小堆:此操作需要 O(logk) 时间,其中 k 是堆中当前元素的数量。在最坏的情况下,k 最多可以是 n,这使得插入操作为 O(logn)。
    2. 从最小堆中移除最小结束时间:此操作也需要 O(logk) 时间,在最坏的情况下为 O(logn)。
  • 由于每个会议都处理一次,并且我们最多执行这些操作 n 次,因此使用堆处理所有会议的总时间复杂度为 O(nlogn)。

总体时间复杂度为 O(nlogn)。这是因为对会议进行排序是时间复杂度的主要因素,而堆操作会增加对数因子。

空间复杂度

排序所需空间

排序是原地进行的,这意味着除了输入列表之外,它只使用少量恒定的额外空间。因此,如果忽略输入列表所需的空间,则排序本身的空间复杂度为 O(1)。

最小堆所需空间

  • 最小堆存储正在进行的会议的结束时间。在最坏的情况下,堆可能需要同时存储所有会议的结束时间,这需要 O(n) 的空间。

输入存储空间

  • 原始会议列表本身需要 O(n) 的空间来存储时间间隔。

总体空间复杂度为 O(n)。这包括输入列表和堆所需的空间。

方法 3:使用扫描线算法

扫描线算法涉及处理一系列事件,就像一条垂直线从左向右扫过时间线一样。在会议室的背景下,目标是确定一个人是否可以在没有任何重叠的情况下参加所有会议。

实施

输出

 
No, the person cannot attend all meetings.   

说明

将会议转换为事件

  • 每个会议都由两个关键的时间点表示:开始时间和结束时间。要应用扫描线算法,这些时刻被转换为离散事件。
  • 对于每个会议,您创建两个事件:
  • 开始事件:在会议开始时发生。
  • 结束事件:在会议结束时发生。

对事件进行排序

  • 为有效处理这些事件,它们是根据时间戳排序的。排序的主要标准是事件发生的时间。
  • 当两个事件具有相同的时间戳时,结束事件比开始事件具有更高的优先级。这一点至关重要,因为如果一个会议在同一时刻结束而另一个会议开始,我们需要在开始之前处理结束,以避免错误的重叠检测。

处理事件

  • 当您按排序顺序处理每个事件时,您会维护一个计数器来跟踪正在进行的会议数量。
  • 处理开始事件:每次遇到开始事件时,计数器就会增加。这表明新会议已经开始,并且正在与正在进行的会议重叠。
  • 处理结束事件:每次遇到结束事件时,计数器就会减少。这表明会议已结束,正在减少正在进行的会议数量。

检测重叠

  • 在事件处理期间,持续监视计数器:
  • 如果计数器在任何时候超过 1,则表示会议发生重叠。因此,您无法在没有冲突的情况下参加所有会议。
  • 如果在整个过程中计数器保持在 1 或更少,则表示所有会议的安排都不会重叠,可以参加所有会议。

复杂度分析

时间复杂度

事件生成

  • 每个会议会生成两个事件(开始和结束)。因此,如果有 n 个会议,将有 2n 个事件。
  • 生成这些事件的复杂度为 O(n),因为每个会议都贡献恒定的工作量。

对事件进行排序

  • 对事件进行排序需要 O(mlogm) 时间,其中 m 是事件的数量。由于 m=2n(每个会议两个事件),排序的复杂度为 O(nlogn)。排序是此方法中最耗时的步骤。

处理事件

  • 处理每个事件包括更新计数器和检查条件。由于每个事件都恰好处理一次,因此此步骤的复杂度为 O(m),其中 m 是事件的数量,在本例中为 O(n)。

将这些步骤结合起来,总体时间复杂度由排序步骤决定:O(nlogn)

空间复杂度

事件存储

  • 您需要存储所有事件。有 n 个会议生成 2n 个事件,这需要 O(n) 的空间。

附加变量

  • 需要空间来存储计数器和其他几个变量,但这些需要恒定的空间。

输入存储

  • 原始会议列表需要 O(n) 的空间。

将这些结合起来,总体空间复杂度为 O(n)。