十字路口的交通

标签: 数组 字符串 动态规划

难度: Hard

前往「力扣挑战赛」场馆的道路上,有一个拥堵的十字路口,该十字路口由两条双向两车道的路交叉构成。由于信号灯故障,交警需要手动指挥拥堵车辆。假定路口没有新的来车且一辆车从一个车道驶入另一个车道所需的时间恰好为一秒钟,长度为 4 的一维字符串数组 `directions` 中按照 **东、南、西、北** 顺序记录了四个方向从最靠近路口到最远离路口的车辆计划开往的方向。其中: - `"E"` 表示向东行驶; - `"S"` 表示向南行驶; - `"W"` 表示向西行驶; - `"N"` 表示向北行驶。 交警每秒钟只能指挥各个车道距离路口最近的一辆车,且每次指挥需要满足如下规则: - 同一秒钟内,一个方向的车道只允许驶出一辆车; - 同一秒钟内,一个方向的车道只允许驶入一辆车; - 同一秒钟内,车辆的行驶路线不可相交。 请返回最少需要几秒钟,该十字路口等候的车辆才能全部走完。 各个车道驶出的车辆可能的行驶路线如图所示: ![图片.png](https://pic.leetcode-cn.com/1630393755-gyPeMM-%E5%9B%BE%E7%89%87.png){:height="350px"} **注意:** - 测试数据保证不会出现掉头行驶指令,即某一方向的行驶车辆计划开往的方向不会是当前车辆所在的车道的方向; - 表示堵塞车辆行驶方向的字符串仅用大写字母 `"E"`,`"N"`,`"W"`,`"S"` 表示。 **示例 1:** >输入:`directions = ["W","N","ES","W"]` > >输出:`2` > >解释: >第 1 秒:东西方向排在最前的车先行,剩余车辆状态 `["","N","S","W"]`; >第 2 秒:南、西、北方向的车行驶,路口无等待车辆; >因此最少需要 2 秒,返回 2。 **示例 2:** >输入:`directions = ["NS","WE","SE","EW"]` > >输出:`3` > >解释: >第 1 秒:四个方向排在最前的车均可驶出; >第 2 秒:东南方向的车驶出,剩余车辆状态 `["","","E","W"]`; >第 3 秒:西北方向的车驶出。 **提示:** - `directions.length = 4` - `0 <= directions[i].length <= 20`

Submission

运行时间: 4296 ms

内存: 58.8 MB

import math
from typing import List
from itertools import combinations
from functools import lru_cache


class Solution:
    def trafficCommand(self, directions: List[str]) -> int:
        """

        :param directions:
        :return:
        """
        ll = [len(i) for i in directions]

        @lru_cache(None)
        def can_pass(e, s, w, n):

            todo = {}
            op = [0] * 16
            if e != '':
                todo['e'] = e.lower()
            if s != '':
                todo['s'] = s.lower()
            if w != '':
                todo['w'] = w.lower()
            if n != '':
                todo['n'] = n.lower()
            for i in todo:
                if i == 'e':
                    if todo[i] == 's':
                        if op[5] == 1 or op[8] == 1 or op[10] == 1 or op[15] == 1 or op[14] == 1:
                            return False
                        op[5] = 1
                        op[8] = 1
                        op[10] = 1
                        op[15] = 1
                        op[14] = 1
                    if todo[i] == 'w':
                        if op[2] == 1 or op[3] == 1 or op[4] == 1 or op[5] == 1:
                            return False
                        op[2] = 1
                        op[3] = 1
                        op[4] = 1
                        op[5] = 1
                    if todo[i] == 'n':
                        if op[1] == 1 or op[5] == 1:
                            return False
                        op[1] = 1
                        op[5] = 1
                if i == 's':
                    if todo[i] == 'w':
                        if op[2] == 1 or op[7] == 1 or op[11] == 1 or op[13] == 1 or op[15] == 1:
                            return False
                        op[2] = 1
                        op[7] = 1
                        op[11] = 1
                        op[13] = 1
                        op[15] = 1
                    if todo[i] == 'e':
                        if op[9] == 1 or op[11] == 1:
                            return False
                        op[9] = 1
                        op[11] = 1
                    if todo[i] == 'n':
                        if op[1] == 1 or op[4] == 1 or op[8] == 1 or op[11] == 1:
                            return False
                        op[1] = 1
                        op[4] = 1
                        op[8] = 1
                        op[11] = 1
                if i == 'w':
                    if todo[i] == 's':
                        if op[6] == 1 or op[10] == 1:
                            return False
                        op[6] = 1
                        op[10] = 1
                    if todo[i] == 'e':
                        if op[6] == 1 or op[7] == 1 or op[9] == 1 or op[8] == 1:
                            return False
                        op[6] = 1
                        op[7] = 1
                        op[9] = 1
                        op[8] = 1
                    if todo[i] == 'n':
                        if op[1] == 1 or op[3] == 1 or op[6] == 1 or op[12] == 1 or op[13] == 1:
                            return False
                        op[1] = 1
                        op[3] = 1
                        op[6] = 1
                        op[12] = 1
                        op[13] = 1

                if i == 'n':
                    if todo[i] == 's':
                        if op[3] == 1 or op[0] == 1 or op[10] == 1 or op[7] == 1:
                            return False
                        op[3] = 1
                        op[0] = 1
                        op[10] = 1
                        op[7] = 1

                    if todo[i] == 'e':
                        if op[9] == 1 or op[0] == 1 or op[4] == 1 or op[12] == 1 or op[14] == 1:
                            return False
                        op[9] = 1
                        op[0] = 1
                        op[4] = 1
                        op[12] = 1
                        op[14] = 1

                    if todo[i] == 'w':
                        if op[0] == 1 or op[2] == 1:
                            return False
                        op[0] = 1
                        op[2] = 1
            return True

        @lru_cache(None)
        def get_min_time(rest_idx: tuple):
            if sum(rest_idx) == 0:
                return 0
            topick = [k for k, v in enumerate(rest_idx) if v > 0]
            ans = math.inf
            for i in topick:
                tp = list(rest_idx)
                tp[i] -= 1
                ans1 = 1 + get_min_time(tuple(tp))
                ans = min(ans, ans1)
            for i in range(2, len(topick) + 1):
                for j in combinations(topick, i):
                    picked = j
                    e, s, w, n = '', '', '', ''
                    if 0 in picked:
                        e = directions[0][len(directions[0]) - rest_idx[0]]
                    if 1 in picked:
                        s = directions[1][len(directions[1]) - rest_idx[1]]
                    if 2 in picked:
                        w = directions[2][len(directions[2]) - rest_idx[2]]
                    if 3 in picked:
                        n = directions[3][len(directions[3]) - rest_idx[3]]
                    ret1 = can_pass(e, s, w, n)
                    if ret1:
                        tp = list(rest_idx)
                        for x in picked:
                            tp[x] -= 1
                            ans1 = 1 + get_min_time(tuple(tp))
                            ans = min(ans, ans1)
            return ans

        ret = get_min_time(tuple(ll))
        return ret


a = Solution()
print(a.trafficCommand(["NN","WE","EN","EW"]))
# print(a.trafficCommand(["S", "W", "N", "E"]))
# # print(a.trafficCommand(directions = ["W","N","ES","W"]))
# print(a.trafficCommand(directions=["NS", "WE", "SE", "EW"]))

Explain

该题解采用了递归加备忘录的方法来解决问题,涉及动态规划的思路。具体思路如下:首先,对每个方向的车辆数进行统计,并定义两个缓存函数。第一个缓存函数`can_pass`用于检查在不产生冲突的情况下哪些车可以同时通过交叉口。它使用了一个16位的数组来标记可能的冲突,并对每一种行驶方向的车辆进行冲突检查。第二个缓存函数`get_min_time`是主要的递归函数,用于计算清空路口所需的最短时间。它首先考虑一个车辆通过的情况,然后尝试所有可能的车辆组合,递归地计算通过剩余车辆所需的时间。这里使用了组合生成和对每种可能的行车情况进行模拟,通过调用`can_pass`来确保没有冲突。

时间复杂度: O(2^n)

空间复杂度: O(n + 4^n)

import math
from typing import List
from itertools import combinations
from functools import lru_cache


class Solution:
    def trafficCommand(self, directions: List[str]) -> int:
        # 计算每个方向的车辆数
        ll = [len(i) for i in directions]

        # 使用备忘录优化,判断是否可通行
        @lru_cache(None)
        def can_pass(e, s, w, n):
            todo = {}
            op = [0] * 16 # 标记16种可能的车辆动作组合
            # 判断每个方向是否有车并转化为小写字母表示方向
            if e != '':
                todo['e'] = e.lower()
            if s != '':
                todo['s'] = s.lower()
            if w != '':
                todo['w'] = w.lower()
            if n != '':
                todo['n'] = n.lower()
            for i in todo:
                # 根据车辆的目标方向,检查是否会产生冲突
                # 例如东边的车向南行驶需要的路径是否被其他车辆占用
                if i == 'e':
                    if todo[i] == 's':
                        if op[5] == 1 or op[8] == 1 or op[10] == 1 or op[15] == 1 or op[14] == 1:
                            return False # 如果路径已被占用则返回不可通行
                        op[5] = 1
                        op[8] = 1
                        op[10] = 1
                        op[15] = 1
                        op[14] = 1
                    # 同样的检查逻辑应用于其他方向和目标组合
                # 下面代码块遵循同样逻辑处理南、西、北方向的车辆

        # 计算最小通过时间的函数
        @lru_cache(None)
        def get_min_time(rest_idx: tuple):
            if sum(rest_idx) == 0:
                return 0 # 所有车辆已通过时返回0
            topick = [k for k, v in enumerate(rest_idx) if v > 0] # 选择还有车辆的方向
            ans = math.inf
            # 单车通过
            for i in topick:
                tp = list(rest_idx)
                tp[i] -= 1
                ans1 = 1 + get_min_time(tuple(tp))
                ans = min(ans, ans1)
            # 尝试多车同时通过的情况,保证不冲突
            for i in range(2, len(topick) + 1):
                for j in combinations(topick, i):
                    picked = j
                    e, s, w, n = '', '', '', ''
                    # 获取对应方向车辆的当前行驶方向
                    if 0 in picked:
                        e = directions[0][len(directions[0]) - rest_idx[0]]
                    if 1 in picked:
                        s = directions[1][len(directions[1]) - rest_idx[1]]
                    if 2 in picked:
                        w = directions[2][len(directions[2]) - rest_idx[2]]
                    if 3 in picked:
                        n = directions[3][len(directions[3]) - rest_idx[3]]
                    ret1 = can_pass(e, s, w, n) # 检查是否冲突
                    if ret1:
                        tp = list(rest_idx)
                        for x in picked:
                            tp[x] -= 1
                        ans1 = 1 + get_min_time(tuple(tp))
                        ans = min(ans, ans1)
            return ans

        ret = get_min_time(tuple(ll))
        return ret

# 示例调用
a = Solution()
print(a.trafficCommand(["NN","WE","EN","EW"]))
# print(a.trafficCommand(["S", "W", "N", "E"]))
# # print(a.trafficCommand(directions = ["W","N","ES","W"]))
# print(a.trafficCommand(directions=["NS", "WE", "SE", "EW"]))

Explore

函数`can_pass`通过一个16位的数组`op`来记录和管理冲突。每一位代表一个特定的车辆方向和目标方向的组合。例如,如果东边的车想向南行驶,这将会影响到其他可能与之冲突的路线(如其他车辆也想向南或交叉这条路线)。通过设置和检查这些位的状态,可以确保任何车辆行进的组合都不会违反交通规则。在实现时,需要确保每一种可能的车辆方向组合都被考虑到,并且对应的位正确地反映了是否会产生冲突。这样,通过逻辑检查和位操作来确保不会有违反交通规则的车辆组合被允许通过。

在递归逻辑中,从两辆车开始尝试主要是基于优化和实际场景的考虑。首先,单车通过是基本情况,必须先考虑。接着,从两辆车开始尝试是因为这是最简单的多车交互情况,可以有效地处理多数交通冲突和优化通行时间。如果直接从更多车辆开始,虽然可能在某些情况下更快,但会大幅增加复杂度和计算量,因为组合数量随车辆数量呈指数增长。因此,从两辆车开始,依次增加车辆数,是一个平衡计算效率和解决问题复杂度的实用方法。

在`get_min_time`函数中,使用了一个元组`rest_idx`来记录每个方向剩余的车辆数量。这个元组在每次递归调用时都会被更新,以反映车辆通过交叉口后的新状态。使用列表推导和元组操作来确保每次只减少通过的车辆数,而不影响未通过的车辆。此外,使用`lru_cache`装饰器对这个函数进行了缓存,可以避免重复计算相同的车辆状态,从而提高效率和防止错误重复处理。这种方法确保了状态的正确更新和高效的递归调用。