scala中的拓扑排序

时间:2017-01-26 17:01:55

标签: scala graph-algorithm

我正在scala中寻找一个很好的topological sorting实现。

解决方案应该是稳定的:

  • 如果输入已经排序,则输出应保持不变

  • 算法应该是确定性的(hashCode无效)

我怀疑有些库可以做到这一点,但由于这个原因,我不想添加非平凡的依赖项。

示例问题:

case class Node(name: String)(val referenced: Node*)

val a = Node("a")()
val b = Node("b")(a)
val c = Node("c")(a)
val d = Node("d")(b, c)
val e = Node("e")(d)
val f = Node("f")()

assertEquals("Previous order is kept", 
   Vector(f, a, b, c, d, e), 
   topoSort(Vector(f, a, b, c, d, e)))

assertEquals(Vector(a, b, c, d, f, e), 
   topoSort(Vector(d, c, b, f, a, e)))

这里定义了顺序,如果节点是在引用其他声明的编程语言中声明声明,那么结果顺序将是 在声明之前不要使用任何声明。

2 个答案:

答案 0 :(得分:1)

这是一个纯粹的功能实现,只有在图形是非循环的情况下才会返回拓扑排序。

case class Node(label: Int)
case class Graph(adj: Map[Node, Set[Node]]) {
  case class DfsState(discovered: Set[Node] = Set(), activeNodes: Set[Node] = Set(), tsOrder: List[Node] = List(),
                      isCylic: Boolean = false)

  def dfs: (List[Node], Boolean) = {
    def dfsVisit(currState: DfsState, src: Node): DfsState = {
      val newState = currState.copy(discovered = currState.discovered + src, activeNodes = currState.activeNodes + src,
        isCylic = currState.isCylic || adj(src).exists(currState.activeNodes))

      val finalState = adj(src).filterNot(newState.discovered).foldLeft(newState)(dfsVisit(_, _))
      finalState.copy(tsOrder = src :: finalState.tsOrder, activeNodes = finalState.activeNodes - src)
    }

    val stateAfterSearch = adj.keys.foldLeft(DfsState()) {(state, n) => if (state.discovered(n)) state else dfsVisit(state, n)}
    (stateAfterSearch.tsOrder, stateAfterSearch.isCylic)
  }

  def topologicalSort: Option[List[Node]] = dfs match {
    case (topologicalOrder, false) => Some(topologicalOrder)
    case _ => None
  }
}

答案 1 :(得分:0)

这是我自己的解决方案。另外,它返回在输入中检测到的可能循环。

节点的格式不固定,因为调用者提供了访问者 将接受一个节点和一个回调,并为每个被引用的节点调用回调。

如果不需要循环报告,则应该很容易删除。

import scala.collection.mutable

// Based on https://en.wikipedia.org/wiki/Topological_sorting?oldformat=true#Depth-first_search
object TopologicalSort {

  case class Result[T](result: IndexedSeq[T], loops: IndexedSeq[IndexedSeq[T]])

  type Visit[T] = (T) => Unit

  // A visitor is a function that takes a node and a callback.
  // The visitor calls the callback for each node referenced by the given node.
  type Visitor[T] = (T, Visit[T]) => Unit

  def topoSort[T <: AnyRef](input: Iterable[T], visitor: Visitor[T]): Result[T] = {

    // Buffer, because it is operated in a stack like fashion
    val temporarilyMarked = mutable.Buffer[T]()

    val permanentlyMarked = mutable.HashSet[T]()

    val loopsBuilder = IndexedSeq.newBuilder[IndexedSeq[T]]

    val resultBuilder = IndexedSeq.newBuilder[T]

    def visit(node: T): Unit = {
      if (temporarilyMarked.contains(node)) {

        val loopStartIndex = temporarilyMarked.indexOf(node)
        val loop = temporarilyMarked.slice(loopStartIndex, temporarilyMarked.size)
          .toIndexedSeq
        loopsBuilder += loop

      } else if (!permanentlyMarked.contains(node)) {

        temporarilyMarked += node

        visitor(node, visit)

        permanentlyMarked += node
        temporarilyMarked.remove(temporarilyMarked.size - 1, 1)
        resultBuilder += node
      }
    }

    for (i <- input) {
      if (!permanentlyMarked.contains(i)) {
        visit(i)
      }
    }

    Result(resultBuilder.result(), loopsBuilder.result())
  }
}

在问题的例子中,这将应用如下:

import TopologicalSort._

def visitor(node: BaseNode, callback: (Node) => Unit): Unit = {
  node.referenced.foreach(callback)
}

assertEquals("Previous order is kept", 
  Vector(f, a, b, c, d, e), 
  topoSort(Vector(f, a, b, c, d, e), visitor).result)

assertEquals(Vector(a, b, c, d, f, e), 
  topoSort(Vector(d, c, b, f, a, e), visitor).result)

关于复杂性的一些想法:

此解决方案的最坏情况复杂度实际上高于O(n + m),因为为每个节点扫描temporarilyMarked数组。

如果将temporarilyMarked替换为例如HashSet,则会提高渐近复杂度。

如果标记直接存储在节点内,则可以实现真正的O(n + m),但将它们存储在外部会使编写通用解决方案变得更容易。

我还没有进行任何性能测试,但我怀疑扫描temporarilyMarked数组即使在大型图表中也不是问题,只要它们不是很深。

Github上的示例代码和测试

我的代码非常相似published here。那个version has a test suite可以用来试验和探索实现。

为什么要检测循环

检测循环可能很有用,例如在大多数数据可以作为DAG处理的序列化情况下,但循环可以通过某种特殊的排列来处理。

上面链接的Github代码中的测试套件包含具有多个循环的各种情况。

相关问题