青训营X豆包MarsCode 技术训练营第五课 | 豆包MarsCode AI 刷题

119 阅读5分钟

学习笔记:计算任务并发数

今天我学习并实现了一个经典的任务调度问题:计算最大并发数。问题描述中给定了多个任务,每个任务都有开始时间和持续时间,要求计算在任何时刻,最多有多少个任务同时进行。这个问题涉及到如何高效地跟踪多个任务的时间段,并找出在某个时刻的并发最大值。

问题理解

我们有 ( n ) 个任务,每个任务由开始时间 ( x ) 和持续时间 ( y ) 组成。任务开始的时间为 ( x ),持续时间为 ( y ),因此任务的结束时间为 ( x + y )。我们的目标是计算出在同一时刻最多有多少个任务正在进行。换句话说,就是我们需要找出最大并发数。

例如,给定输入:

n = 4 array = [[1, 2], [2, 3], [3, 5], [4, 3]]

其中: 第一个任务在第1秒开始,持续2秒(1 - 3秒)。 第二个任务在第2秒开始,持续3秒(2 - 5秒)。 第三个任务在第3秒开始,持续5秒(3 - 8秒)。 第四个任务在第4秒开始,持续3秒(4 - 7秒)。

我们需要找到,在任何时刻,最多有多少任务重叠。正确答案是3(在时间段 [3, 4] 时,三任务重叠)。

思路与解法

这个问题可以通过事件驱动的方法来解决。我们将任务的开始和结束分别视为两个“事件”。对于每个任务,生成两个事件:

  1. 任务开始时,并发数增加(+1)。
  2. 任务结束时,并发数减少(-1)。

我们将所有事件按时间排序,时间相同的情况下,结束事件先处理(因为结束事件的并发数减少,开始事件的并发数增加)。

接着,我们通过遍历这些事件,维护当前的并发数,并在遍历过程中更新最大并发数。

步骤

  1. 生成事件:对每个任务,生成两个事件——一个是任务开始,另一个是任务结束。
  2. 排序事件:按时间排序事件,如果时间相同,则结束事件排在前面。
  3. 遍历事件:遍历所有事件,计算当前并发数,更新最大并发数。

代码实现

def solution(n, array): events = []

# 将每个任务的开始和结束时间分别作为事件
for task in array:
    start, duration = task
    end = start + duration
    events.append((start, 1))  # 任务开始,增加并发数
    events.append((end, -1))   # 任务结束,减少并发数

# 按照时间排序,时间相同则结束事件排在前面
events.sort(key=lambda x: (x[0], x[1]))

max_concurrent = 0  # 记录最大并发数
current_concurrent = 0  # 当前并发数

# 遍历事件,更新并发数
for event in events:
    current_concurrent += event[1]
    max_concurrent = max(max_concurrent, current_concurrent)

return max_concurrent

测试用例

if name == "main": # 测试样例 print(solution(2, [[1, 2], [2, 3]]) == 2) # 输出: 2 print(solution(4, [[1, 2], [2, 3], [3, 5], [4, 3]]) == 3) # 输出: 3 print(solution(5, [[1, 3], [3, 4], [2, 2], [6, 5], [5, 3]]) == 3) # 输出: 3

代码解析

  1. 生成事件:对于每个任务,start 表示任务的开始时间,duration 是任务的持续时间。通过 start + duration 得到任务的结束时间。每个任务产生两个事件,一个是任务开始的 (start, 1),表示并发数增加;另一个是任务结束的 (end, -1),表示并发数减少。

  2. 排序事件:事件排序按照时间来排,时间相同的事件,结束事件会排在前面,因为结束事件是对并发数的减少操作。这样可以确保在同一时刻,任务结束不影响当前并发数的计算。

  3. 遍历事件:遍历所有排序后的事件,并更新当前的并发数 current_concurrent。每处理一个事件,就检查当前并发数是否超过历史最大值,并更新 max_concurrent

  4. 返回结果:最终返回最大并发数 max_concurrent

复杂度分析

时间复杂度:排序需要 ( O(N \log N) ) 的时间复杂度,其中 ( N ) 是事件的数量,最多为 ( 2 \times n ),因此总体时间复杂度为 ( O(n \log n) )。

空间复杂度:需要存储所有的事件,因此空间复杂度为 ( O(n) )。

由于事件的排序和遍历是主要的时间开销,因此算法的时间复杂度可以认为是 ( O(n \log n) ),对于大规模数据输入也是高效的。

测试与验证

以下是几个测试用例,涵盖了常见的场景:

  1. 简单情况:

    print(solution(2, [[1, 2], [2, 3]])) # 输出: 2

    结果是2,因为第1秒到第2秒两个任务都在下载。

  2. 多个任务重叠:

    print(solution(4, [[1, 2], [2, 3], [3, 5], [4, 3]])) # 输出: 3

    结果是3,最多有三个任务在下载的时刻是[3, 4]。

  3. 不完全重叠:

    print(solution(5, [[1, 3], [3, 4], [2, 2], [6, 5], [5, 3]])) # 输出: 3

    结果是3,最多有三个任务在下载的时刻是[3, 5]。

学习总结

事件驱动法是解决这类时间区间重叠问题的有效方法,能够高效地处理并发数计算问题。 排序与处理事件:通过将所有任务的开始和结束时间转化为事件并排序,确保能够在遍历过程中正确计算最大并发数。 通过这种方法,时间复杂度得到了有效控制,适用于更大规模的任务调度问题。

下次我可以尝试通过其他的优化方式,进一步改进算法的效率,或尝试类似问题的变种。