过桥的时间

标签: 数组 模拟 堆(优先队列)

难度: Hard

共有 k 位工人计划将 n 个箱子从旧仓库移动到新仓库。给你两个整数 nk,以及一个二维整数数组 time ,数组的大小为 k x 4 ,其中 time[i] = [leftToRighti, pickOldi, rightToLefti, putNewi]

一条河将两座仓库分隔,只能通过一座桥通行。旧仓库位于河的右岸,新仓库在河的左岸。开始时,所有 k 位工人都在桥的左侧等待。为了移动这些箱子,第 i 位工人(下标从 0 开始)可以:

  • 从左岸(新仓库)跨过桥到右岸(旧仓库),用时 leftToRighti 分钟。
  • 从旧仓库选择一个箱子,并返回到桥边,用时 pickOldi 分钟。不同工人可以同时搬起所选的箱子。
  • 从右岸(旧仓库)跨过桥到左岸(新仓库),用时 rightToLefti 分钟。
  • 将箱子放入新仓库,并返回到桥边,用时 putNewi 分钟。不同工人可以同时放下所选的箱子。

如果满足下面任一条件,则认为工人 i效率低于 工人 j

  • leftToRighti + rightToLefti > leftToRightj + rightToLeftj
  • leftToRighti + rightToLefti == leftToRightj + rightToLeftji > j

工人通过桥时需要遵循以下规则:

  • 如果工人 x 到达桥边时,工人 y 正在过桥,那么工人 x 需要在桥边等待。
  • 如果没有正在过桥的工人,那么在桥右边等待的工人可以先过桥。如果同时有多个工人在右边等待,那么 效率最低 的工人会先过桥。
  • 如果没有正在过桥的工人,且桥右边也没有在等待的工人,同时旧仓库还剩下至少一个箱子需要搬运,此时在桥左边的工人可以过桥。如果同时有多个工人在左边等待,那么 效率最低 的工人会先过桥。

所有 n 个盒子都需要放入新仓库,请你返回最后一个搬运箱子的工人 到达河左岸 的时间。

示例 1:

输入:n = 1, k = 3, time = [[1,1,2,1],[1,1,3,1],[1,1,4,1]]
输出:6
解释:
从 0 到 1 :工人 2 从左岸过桥到达右岸。
从 1 到 2 :工人 2 从旧仓库搬起一个箱子。
从 2 到 6 :工人 2 从右岸过桥到达左岸。
从 6 到 7 :工人 2 将箱子放入新仓库。
整个过程在 7 分钟后结束。因为问题关注的是最后一个工人到达左岸的时间,所以返回 6 。

示例 2:

输入:n = 3, k = 2, time = [[1,9,1,8],[10,10,10,10]]
输出:50
解释:
从 0 到 10 :工人 1 从左岸过桥到达右岸。
从 10 到 20 :工人 1 从旧仓库搬起一个箱子。
从 10 到 11 :工人 0 从左岸过桥到达右岸。
从 11 到 20 :工人 0 从旧仓库搬起一个箱子。
从 20 到 30 :工人 1 从右岸过桥到达左岸。
从 30 到 40 :工人 1 将箱子放入新仓库。
从 30 到 31 :工人 0 从右岸过桥到达左岸。
从 31 到 39 :工人 0 将箱子放入新仓库。
从 39 到 40 :工人 0 从左岸过桥到达右岸。
从 40 到 49 :工人 0 从旧仓库搬起一个箱子。
从 49 到 50 :工人 0 从右岸过桥到达左岸。
从 50 到 58 :工人 0 将箱子放入新仓库。
整个过程在 58 分钟后结束。因为问题关注的是最后一个工人到达左岸的时间,所以返回 50 。

提示:

  • 1 <= n, k <= 104
  • time.length == k
  • time[i].length == 4
  • 1 <= leftToRighti, pickOldi, rightToLefti, putNewi <= 1000

Submission

运行时间: 184 ms

内存: 21.3 MB

class Solution:
    def findCrossingTime(self, n: int, k: int, time: List[List[int]]) -> int:
        time.sort(key=lambda x: x[0] + x[2])
        cur = 0
        wait_in_left, wait_in_right = [], []
        work_in_left, work_in_right = [], []
        for i in range(k):
            heappush(wait_in_left, -i)
        while 1:
            while work_in_left:
                t, i = work_in_left[0]
                if t > cur:
                    break
                heappop(work_in_left)
                heappush(wait_in_left, -i)
            while work_in_right:
                t, i = work_in_right[0]
                if t > cur:
                    break
                heappop(work_in_right)
                heappush(wait_in_right, -i)
            left_to_go = n > 0 and wait_in_left
            right_to_go = bool(wait_in_right)
            if not left_to_go and not right_to_go:
                nxt = inf
                if work_in_left:
                    nxt = min(nxt, work_in_left[0][0])
                if work_in_right:
                    nxt = min(nxt, work_in_right[0][0])
                cur = nxt
                continue
            if right_to_go:
                i = -heappop(wait_in_right)
                cur += time[i][2]
                if n == 0 and not wait_in_right and not work_in_right:
                    return cur
                heappush(work_in_left, (cur + time[i][3], i))
            else:
                i = -heappop(wait_in_left)
                cur += time[i][0]
                n -= 1
                heappush(work_in_right, (cur + time[i][1], i))

Explain

该题解采用优先队列(堆)和模拟的方法来解决问题。首先,将工人根据其过桥时间(来回之和)排序,以优化选择过桥的工人顺序。使用两个优先队列(堆),分别管理在左岸和右岸等待过桥的工人,以及两个列表管理在左岸和右岸工作的工人。通过模拟整个过程,每次选择出下一个行动的工人,并根据当前的情景更新时间和工人的状态。当所有箱子都被搬运完毕后,返回最后一个到达左岸的时间。

时间复杂度: O(nk log k)

空间复杂度: O(k)

class Solution:
    def findCrossingTime(self, n: int, k: int, time: List[List[int]]) -> int:
        time.sort(key=lambda x: x[0] + x[2])  # Sort workers based on efficiency
        cur = 0  # Current time
        wait_in_left, wait_in_right = [], []  # Min-heaps for workers waiting on both sides
        work_in_left, work_in_right = [], []  # Lists of workers working on both sides
        for i in range(k):
            heappush(wait_in_left, -i)  # Initially all workers are at the left side
        while True:
            while work_in_left:  # Process workers who finished putting boxes in the new warehouse
                t, i = work_in_left[0]
                if t > cur:
                    break
                heappop(work_in_left)
                heappush(wait_in_left, -i)  # Return worker to left side wait list
            while work_in_right:  # Process workers who finished picking boxes from the old warehouse
                t, i = work_in_right[0]
                if t > cur:
                    break
                heappop(work_in_right)
                heappush(wait_in_right, -i)  # Return worker to right side wait list
            left_to_go = n > 0 and wait_in_left
            right_to_go = bool(wait_in_right)
            if not left_to_go and not right_to_go:
                nxt = inf
                if work_in_left:
                    nxt = min(nxt, work_in_left[0][0])
                if work_in_right:
                    nxt = min(nxt, work_in_right[0][0])
                cur = nxt
                continue
            if right_to_go:
                i = -heappop(wait_in_right)
                cur += time[i][2]
                if n == 0 and not wait_in_right and not work_in_right:
                    return cur
                heappush(work_in_left, (cur + time[i][3], i))
            else:
                i = -heappop(wait_in_left)
                cur += time[i][0]
                n -= 1
                heappush(work_in_right, (cur + time[i][1], i))  # Send worker to right side to pick a box

Explore

优先队列(堆)被选择用于管理工人状态的原因是它能够高效地支持动态数据集中的插入和删除操作,并且能够快速地访问最小元素(或最大元素)。在此题解中,工人需要根据他们的完成时间(或者开始时间)被优先处理,以最小化总过桥时间。使用优先队列可以确保每次都可以从堆中取出最先完成任务的工人,这对于模拟实时任务调度至关重要。如果使用队列或栈,则只能支持先进先出或后进先出的元素处理,无法有效地处理按特定条件优先的场景,这会导致无法高效地管理和调度工人的状态变化。

在模拟过程中,一个工人从一个状态转移到另一个状态的条件主要依赖于他们的任务完成时间和当前的模拟时间。具体来说,如果工人在左岸完成装箱任务的时间小于或等于当前模拟时间,他们就会转移到等待过桥到右岸的队列中。同理,如果工人在右岸完成取箱任务的时间也小于或等于当前模拟时间,他们就会转移到等待过桥回左岸的队列中。此外,还需要根据剩余的箱子数量n来决定是否需要发送工人从左岸到右岸。当所有任务都结束,且没有工人需要过桥时,模拟结束。状态转移不仅基于工人完成任务的时间,还受到其他工人状态和任务需求的影响。

当工人在一岸完成任务后,他们的下一步去向取决于当前的任务需求和他们所在的位置。如果一个工人在左岸完成装箱任务,并且还有箱子需要搬运到右岸,他将进入等待过桥到右岸的优先队列中。如果一个工人在右岸完成取箱任务,他将进入等待过桥返回左岸的优先队列中。这些决策基于当前的模拟时间和任务的剩余量。优先队列确保了在每个场景中,能够优先处理那些可以最快完成当前任务的工人。此外,如果所有箱子都已搬运完毕,任何在右岸完成任务的工人将直接过桥返回左岸,结束他们的任务。