拓扑排序,支持循环依赖

时间:2014-01-17 15:11:29

标签: c# sorting topological-sort

考虑以下依赖关系(其中A --> B表示B依赖于A,因此有效地,A是'父')

A --> B
A --> C
C --> D
C --> E

更加图形化:

    A
    |
----------
|        |
B        C
         |
    -----------
    |         |
    D         E

拓扑排序算法会返回类似的内容:

ABCDE

我找到了这个代码(exhibit Aexhibit B),但都没有支持cyclice依赖项。我觉得这可能会发生:

A --> B
B --> C
B --> D
C --> B
C --> E

图形:

A
|
B <--> C
|      |
D      E

这可能会返回ABCDEACBDE。因此,因为B和C处于相同的“级别”,它们之间的顺序并不重要(同样对于D和E)。

我怎么能完成这样的事情。我意识到这不是一个拓扑排序,但我不是专家数学家,所以我真的不知道从哪里开始寻找,更不用说如何实现它了。

就我个人而言,我正在使用C#,但如果您知道如何使用其他任何语言进行操作,我将很乐意研究您的代码并将其转换为C#。

更新

我也有以下情况:

A <--------
|         |
--> B --> C
    |     |
    D     E

所以,重要,这不一定是树。我可以有任意图形。实际上,并非所有节点都必须相互连接。

4 个答案:

答案 0 :(得分:19)

首先,如果你有一个图表,你可以问“你依赖什么”,这在概念上会更容易?我将假设我们有一个图表,其中从A到B的有向边意味着“A取决于B”,这与您的陈述相反。

我对你的问题感到有些困惑,因为忽略循环的拓扑排序几乎与常规拓扑排序相同。我将开发算法,以便您可以按照自己的意愿处理周期;也许这会有所帮助。

排序的想法是:

  • 图表是节点的集合,每个节点都有一组邻居。正如我所说,如果节点A有一个邻居B,那么A取决于B,所以B必须在A之前发生。

  • 排序采用图表并生成排序的节点列表。

  • 在排序操作期间,维护字典,将每个节点映射到三个值之一:alive,dead和undead。尚未处理活动节点。已经处理了死节点。正在处理不死节点;它不再存在但尚未死亡。

  • 如果遇到死节点,可以跳过它;它已经在输出列表中了。

  • 如果遇到活动节点,则以递归方式处理它。

  • 如果遇到不死节点,那么它就是循环的一部分。你喜欢什么。 (如果周期是非法的,则产生错误,如果周期合法则将其视为死亡,等等。)


function topoSort(graph) 
  state = []
  list = []
  for each node in graph
    state[node] = alive
  for each node in graph
    visit(graph, node, list, state)
  return list

function visit(graph, node, list, state)
  if state[node] == dead
    return // We've done this one already.
  if state[node] == undead
    return // We have a cycle; if you have special cycle handling code do it here.
  // It's alive. Mark it as undead.
  state[node] = undead
  for each neighbour in getNeighbours(graph, node)
    visit(graph, neighbour, list, state)
  state[node] = dead
  append(list, node);

有意义吗?

答案 1 :(得分:6)

3年后的编辑:自从我在2014年首次实施此功能以来,我偶尔也会回到这里。在我第一次发布这个答案时,我并没有真正理解它,所以答案过于复杂。这种排序实际上非常简单:

public class Node
{
    public int Data { get; set; }
    public List<Node> Children { get; set; }

    public Node()
    {
        Children = new List<Node>();
    }
}

public class Graph
{
    public List<Node> Nodes { get; set; }

    public Graph()
    {
        Nodes = new List<Node>();
    }

    public List<Node> TopologicSort()
    {
        var results = new List<Node>();
        var seen = new List<Node>();
        var pending = new List<Node>();

        Visit(Nodes, results, seen, pending);

        return results;
    }

    private void Visit(List<Node> graph, List<Node> results, List<Node> dead, List<Node> pending)
    {
        // Foreach node in the graph
        foreach (var n in graph)
        {
            // Skip if node has been visited
            if (!dead.Contains(n))
            {
                if (!pending.Contains(n))
                {
                    pending.Add(n);
                }
                else
                {
                    Console.WriteLine(String.Format("Cycle detected (node Data={0})", n.Data));
                    return;
                }

                // recursively call this function for every child of the current node
                Visit(n.Children, results, dead, pending);

                if (pending.Contains(n))
                {
                    pending.Remove(n);
                }

                dead.Add(n);

                // Made it past the recusion part, so there are no more dependents.
                // Therefore, append node to the output list.
                results.Add(n);
            }
        }
    }
}

答案 2 :(得分:1)

实际上,您希望图表的breadth-first打印输出

链接的维基百科页面列出了执行此操作的算法。

SO上还有this question

答案 3 :(得分:1)

首先考虑问题吧。你没有树。你有一个任意的图表。

考虑到这一点,你可能首先需要做的是find cycles并通过删除循环中的边来打破它们(好的,将边标记为“在进行拓扑排序时忽略它”)。

删除所有循环后,您可以将toplogical排序应用于剩余的节点和弧。