Search code examples
scalagraphtail-recursionscala-catstopological-sort

Tail recursive algorithm for generating all topological orderings in a graph


Given a graph i need to generate all topological orderings. For instance, given the following graph:

enter image description here

i want to generate all topological orderings, which are:

  • 2 4 7 5
  • 2 7 4 5
  • 2 4 5 7

Because many topological orderings may exist, I need to generate them lazily. Currently, I have a working implementation that is recursive and works on top of the scala-graph library:

import scalax.collection.Graph
import scalax.collection.GraphPredef._
import scalax.collection.GraphEdge._

import scala.collection.mutable.ArrayStack
import scala.collection.Set

def allTopologicalSorts[T](graph: Graph[T, DiEdge]): Stream[List[graph.NodeT]] = {
  val indegree: Map[graph.NodeT, Int] = graph.nodes.map(node => (node, node.inDegree)).toMap

  def isSource(node: graph.NodeT): Boolean = indegree.get(node).get == 0
  def getSources(): Set[graph.NodeT] = graph.nodes.filter(node => isSource(node))

  def processSources(sources: Set[graph.NodeT], indegrees: Map[graph.NodeT, Int], topOrder: List[graph.NodeT], cnt: Int): Stream[List[graph.NodeT]] = {
    if (sources.nonEmpty) {
      // `sources` contain all the nodes we can pick
      // --> generate all possibilities
      sources.toStream.flatMap(src => {
        val newTopOrder = src :: topOrder
        var newSources = sources - src

        // Decrease the in-degree of all adjacent nodes
        var newIndegrees = indegrees
        for (adjacent <- src.diSuccessors) {
          val newIndeg = newIndegrees.get(adjacent).get - 1
          newIndegrees = newIndegrees.updated(adjacent, newIndeg)
          // If in-degree becomes zero, add to sources
          if (newIndeg == 0) {
            newSources = newSources + adjacent
          }
        }

        processSources(newSources, newIndegrees, newTopOrder, cnt + 1)
      })
    }
    else if (cnt != graph.nodes.size) {
      throw new Error("There is a cycle in the graph.")
    }
    else {
      topOrder.reverse #:: Stream.empty[List[graph.NodeT]]
    }
  }

  processSources(getSources(), indegree, List[graph.NodeT](), 0)
}

Now, i can generate all (or only a few) topological orderings as follows:

val graph: Graph[Int, DiEdge] = Graph(2 ~> 4, 2 ~> 7, 4 ~> 5)
allTopologicalSorts(graph) foreach println

How can i make the algorithm tail recursive but still lazy?


Solution

  • Implementing this variation on topological sort without blowing up the stack and without computing all possibilities at once has been painful. I ended up with the following implementation:

    import scalax.collection.Graph
    import scalax.collection.GraphPredef._
    import scalax.collection.GraphEdge._
    import scala.collection.Set
    
    object test extends App {
    
      class TopSorter[T](val graph: Graph[T, DiEdge]) extends Iterator[List[T]] {
    
        final case class State[Node](indegrees: Map[Node, Int], topo: List[Node])
    
        sealed trait TopoRes
        final case class Res(order: List[graph.NodeT], sorter: Set[State[graph.NodeT]]) extends TopoRes
        final case object Nil extends TopoRes
    
        private[this] val indegs: Map[graph.NodeT, Int] = graph.nodes.map(node => (node, node.inDegree)).toMap
        private[this] var nextOrder = nextTopo(Set(State(indegs, List[graph.NodeT]())))
    
        override def hasNext: Boolean = nextOrder.isInstanceOf[Res]
    
        override def next(): List[T] = nextOrder match {
          case Res(order, sorter) => {
            nextOrder = nextTopo(sorter)
            order.map(_.value)
          }
          case Nil => throw new NoSuchElementException("next on empty iterator")
        }
    
        private def nextTopo(w: Set[State[graph.NodeT]]): TopoRes = {
          if (w.isEmpty) {
            Nil
          }
          else {
            w.head match {
              case State(indegrees, topo) => {
                val sources = indegrees.keySet.filter(indegrees.get(_).get == 0)
                if (sources.isEmpty) {
                  Res(topo.reverse, w.tail) // The result is the order + state to compute the next order
                }
                else {
                  sourcesLoop(sources, w.tail, topo, indegrees)
                }
              }
            }
          }
        }
    
        private def sourcesLoop(sources: Set[graph.NodeT], w: Set[State[graph.NodeT]], topo: List[graph.NodeT], indegrees: Map[graph.NodeT, Int]): TopoRes = {
          if (sources.isEmpty) {
            nextTopo(w)
          }
          else {
            val source = sources.head
            succLoop(source.diSuccessors, indegrees - source, sources, w, source, topo, indegrees)
          }
        }
    
        private def succLoop(succs: Set[graph.NodeT], indegrees: Map[graph.NodeT, Int], sources: Set[graph.NodeT], w: Set[State[graph.NodeT]], source: graph.NodeT, topo: List[graph.NodeT], oldIndegrees: Map[graph.NodeT, Int]): TopoRes = {
          if (succs.isEmpty) {
            sourcesLoop(sources.tail, w + State(indegrees, source :: topo), topo, oldIndegrees)
          }
          else {
            val succ = succs.head
            succLoop(succs.tail, indegrees.updated(succ, indegrees.get(succ).get - 1), sources, w, source, topo, oldIndegrees)
          }
        }
      }
    
      val graph: Graph[Int, DiEdge] = Graph(2 ~> 4, 2 ~> 7, 4 ~> 5)
      val it = new TopSorter(graph)
    
      while (it.hasNext)
        println(it.next())
    }