Tarjan强连通组件算法的功能实现

时间:2013-04-08 11:24:41

标签: scala clojure functional-programming graph-algorithm tarjans-algorithm

我继续在Scala中implemented textbook version of Tarjan's SCC algorithm。但是,我不喜欢代码 - 它是非常必要的/程序性的,有很多变异的状态和簿记索引。是否有更多"功能"算法的版本?我相信算法的命令式版本隐藏了算法背后的核心思想,而不像功能版本。我发现someone else encountering the same problem使用了这个特殊的算法,但是我无法将他的Clojure代码翻译成idomatic Sc​​ala。

注意:如果有人想要试验,我有一个很好的设置,可以生成随机图表和tests your SCC algorithm vs running Floyd-Warshall

3 个答案:

答案 0 :(得分:10)

请参阅David King和John Launchbury的Lazy Depth-First Search and Linear Graph Algorithms in Haskell。它以功能样式描述了许多图算法,包括SCC。

答案 1 :(得分:9)

以下功能性Scala代码生成一个地图,用于为图表的每个节点分配代表。每个代表都标识一个强关联组件。该代码基于Tarjan的强连通组件算法。

为了理解算法,理解dfs函数的折叠和契约可能就足够了。

def scc[T](graph:Map[T,Set[T]]): Map[T,T] = {
  //`dfs` finds all strongly connected components below `node`
  //`path` holds the the depth for all nodes above the current one
  //'sccs' holds the representatives found so far; the accumulator
  def dfs(node: T, path: Map[T,Int], sccs: Map[T,T]): Map[T,T] = {
    //returns the earliest encountered node of both arguments
    //for the case both aren't on the path, `old` is returned
    def shallowerNode(old: T,candidate: T): T = 
      (path.get(old),path.get(candidate)) match {
        case (_,None) => old
        case (None,_) => candidate
        case (Some(dOld),Some(dCand)) =>  if(dCand < dOld) candidate else old
      }

    //handle the child nodes
    val children: Set[T] = graph(node)
    //the initially known shallowest back-link is `node` itself
    val (newState,shallowestBackNode) = children.foldLeft((sccs,node)){
      case ((foldedSCCs,shallowest),child) =>
        if(path.contains(child))
          (foldedSCCs, shallowerNode(shallowest,child))
        else {
          val sccWithChildData = dfs(child,path + (node -> path.size),foldedSCCs)
          val shallowestForChild = sccWithChildData(child)
          (sccWithChildData, shallowerNode(shallowest, shallowestForChild))
        }
    }

    newState + (node -> shallowestBackNode)
  }

  //run the above function, so every node gets visited
  graph.keys.foldLeft(Map[T,T]()){ case (sccs,nextNode) =>
    if(sccs.contains(nextNode))
      sccs
    else
      dfs(nextNode,Map(),sccs)
  }
}

我只在维基百科页面上的示例图表上测试了代码。

与命令式版本的区别

与原始实现相比,我的版本避免显式展开堆栈并简单地使用正确的(非尾部)递归函数。堆栈由名为path的持久映射表示。在我的第一个版本中,我使用List作为堆栈;但这样效率较低,因为必须搜索包含元素的内容。

效率

代码相当高效。对于每个边缘,您必须更新和/或访问不可变地图path,其费用为O(log|N|),共计O(|E| log|N|)。这与命令式版本实现的O(|E|)形成鲜明对比。

线性时间实施

Chris Okasaki回答的论文给出了Haskell中线性时间解决方案,用于寻找强连通分量。它们的实现基于Kosaraju的查找SCC的算法,它基本上需要两次深度优先遍历。该论文的主要贡献似乎是Haskell中一个懒惰的线性时间DFS实现。

实现线性时间解决方案所需的是具有O(1)单例添加和成员资格测试的集合。这基本上是同样的问题,使得在这个答案中给出的解决方案具有比命令性解决方案更高的复杂性。他们在Haskell中使用状态线程解决它,也可以在Scala中完成(参见Scalaz)。因此,如果有人愿意使代码变得相当复杂,则可以将Tarjan的SCC算法实现为功能O(|E|)版本。

答案 2 :(得分:0)

查看https://github.com/jordanlewis/data.union-find,这是算法的Clojure实现。它伪装成一种数据结构,但算法就在那里。当然,它纯粹是功能性的。