📜  为边分配方向,使有向图保持无环(1)

📅  最后修改于: 2023-12-03 15:06:18.127000             🧑  作者: Mango

为边分配方向,使有向图保持无环

在有向图中,如果存在一个环路,那么就无法通过一系列的有向边到达终点。因此,在许多情况下,我们需要为边分配方向,使有向图保持无环。这个问题被称为有向无环图(Directed Acyclic Graph, DAG)。

思路

有向无环图(DAG)可以通过拓扑排序来实现,拓扑排序将有向图中的所有节点排列成一个线性序列,使得对于任何一条有向边(u,v),节点u在序列中都排在节点v的前面。我们可以使用拓扑排序来为有向图中的边分配方向。

具体的做法是,首先,我们需要找到任意一个没有前驱的节点,并将其作为序列的开头;然后,我们需要从图中删除该节点以及以该节点为起点的所有边;接着,我们继续找到没有前驱的节点,并将其加入到序列中。如此重复,直到所有的节点都加入到序列中。

如果在进行拓扑排序的过程中,我们无法找到一个没有前驱的节点,那么说明该图不是一个有向无环图(DAG)。

实现

我们可以使用两种方法来实现拓扑排序:Kahn算法和DFS算法。

Kahn算法

Kahn算法使用一个入度数组来存储每个节点的入度,一个队列来存储所有入度为0的节点。首先,我们要初始化入度数组,然后将每个入度为0的节点加入到队列中。接着,我们开始遍历队列,对于队列中的每个节点,我们遍历所有以该节点为起点的边,并减小相应的入度值。如果减小后的入度值为0,则将该节点加入队列中。如此重复,直到队列为空。

找到任意一个没有前驱的节点的时间复杂度是O(N),最坏情况下,我们需要遍历所有的节点,因此,整个算法的时间复杂度为O(N+E),其中N为节点数,E为边数。

def topological_sort(graph):
    """
    Kahn算法拓扑排序
    :param graph: 以邻接表形式表示的图
    :return: 排序结果,如果存在环路则返回空列表
    """
    # 初始化入度数组
    in_degrees = [0] * len(graph)
    for u in graph:
        for v in graph[u]:
            in_degrees[v] += 1

    # 将所有入度为0的节点加入到队列中
    queue = [u for u in range(len(graph)) if in_degrees[u] == 0]

    # 开始遍历队列
    result = []
    while queue:
        u = queue.pop(0)
        result.append(u)
        for v in graph[u]:
            in_degrees[v] -= 1
            if in_degrees[v] == 0:
                queue.append(v)

    # 如果存在环路则返回空列表
    if len(result) != len(graph):
        return []

    return result
DFS算法

DFS算法使用一个栈来存储已经遍历但是未确定顺序的节点,以及一个visited数组来记录节点是否被访问过。首先,我们开始遍历每个节点,对于每个未被访问的节点,我们将其加入到栈中,并从当前节点开始进行DFS遍历。在遍历过程中,如果发现了一个已经被访问过但是未确定顺序的节点,那么说明存在环路,无法进行拓扑排序。如果遍历完成后没有发现环路,则从栈顶依次弹出节点,并将其加入结果序列中。

DFS算法与Kahn算法不同,它不需要单独地初始化入度数组,而是在遍历的过程中动态计算入度。

def dfs_topological_sort(graph):
    """
    DFS算法拓扑排序
    :param graph: 以邻接表形式表示的图
    :return: 排序结果,如果存在环路则返回空列表
    """
    # 采用颜色标记节点状态
    # 0 表示未访问
    # 1 表示已访问但是未确定顺序
    # 2 表示已访问且已确定顺序
    color = [0] * len(graph)

    # 结果序列
    result = []

    def dfs(u):
        """
        深度优先遍历
        """
        nonlocal color, result

        # 标记为已访问但未确定顺序
        color[u] = 1

        # 遍历以该节点为起点的所有边
        for v in graph[u]:
            # 如果该节点未被访问,则进行DFS遍历
            if color[v] == 0:
                dfs(v)
                # 如果在遍历的过程中发现了一个环路,则返回空列表
                if not result:
                    return
            # 如果该节点已经被访问但未确定顺序,则说明存在环路,返回空列表
            elif color[v] == 1:
                result = []
                return

        # 标记为已访问且已确定顺序,并将节点加入结果序列
        color[u] = 2
        result.append(u)

    # 开始遍历每个节点
    for u in range(len(graph)):
        if color[u] == 0:
            dfs(u)
            # 如果发现存在环路,则直接返回空列表
            if not result:
                return []

    # 返回结果序列
    return result[::-1]
总结

为有向图分配方向,使其保持无环的问题,可以通过拓扑排序来实现。拓扑排序的基本思路是找到任意一个没有前驱的节点,并将其作为序列的开头;然后,从图中删除该节点以及以该节点为起点的所有边;接着,再次找到没有前驱的节点,并将其加入到序列中。如此重复,直到所有的节点都加入到序列中。如果在进行拓扑排序的过程中,我们无法找到一个没有前驱的节点,那么说明该图不是一个有向无环图(DAG)。

拓扑排序可以使用两种方法实现:Kahn算法和DFS算法。Kahn算法使用入度数组和队列来实现,时间复杂度为O(N+E)。DFS算法使用颜色标记节点状态和栈来实现,时间复杂度同样为O(N+E)。两种算法的实现都需要考虑到存在环路的情况,如果发现了环路,则返回空列表。