【问题标题】:Manually transforming tree recursion into tail recursion using a stack使用堆栈手动将树递归转换为尾递归
【发布时间】:2019-05-02 08:20:39
【问题描述】:

我正在实现拓扑排序的变体(在scala-graph 之上),它返回所有拓扑排序,而不仅仅是一个。我有一个树递归实现,我想让尾递归。我不想使用蹦床,而是想模仿this answer 中描述的调用堆栈。

这是我的算法的树递归版本:

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

def allTopologicalSorts[T](graph: Graph[T, DiEdge]): Unit = {
  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))

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

  def processSources(sources: Set[graph.NodeT], indegrees: Map[graph.NodeT, Int], topOrder: List[graph.NodeT], cnt: Int): Unit = {
    if (sources.nonEmpty) {
      // `sources` contain all the nodes we can pick
      // --> generate all possibilities
      for (src <- sources) {
        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) {
      println("There is a cycle in the graph.")
    }
    else {
      println(topOrder.reverse)
    }
  }
}

我们可以如下运行算法

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

正确返回

  • 列表(2、7、4、5)
  • 列表(2、4、7、5)
  • 列表(2、4、5、7)

现在,我尝试通过手动保留堆栈来实现尾递归版本

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

def allTopologicalSorts[T](graph: Graph[T, DiEdge]): Unit = { 
  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]): Unit = {
    type Order = List[graph.NodeT]
    case class Frame(sources: List[graph.NodeT], indegrees: Map[graph.NodeT, Int], topOrder: Order, cnt: Int)

    def step(stack: List[Frame]): Unit = {
      stack match {
        case Frame(src :: rest, indegrees, topOrder, cnt) :: tail => {
          val onBacktrackingFrame = Frame(rest, indegrees, topOrder, cnt)

          // Process src now and remember to do the rest later
          val newTopOrder = src :: topOrder
          var newSources = rest

          // 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 = adjacent :: newSources
            }
          }

          val recursionFrame = Frame(newSources, newIndegrees, newTopOrder, cnt + 1)
          step(recursionFrame :: onBacktrackingFrame :: tail)
        }
        case Frame(Nil, indegrees, topOrder, cnt) :: tail => {
          println(topOrder.reverse)
          step(tail)
        }
        case Nil =>
      }
    }

    step(List(Frame(sources.toList, indegrees, List[graph.NodeT](), 0)))
  }

  processSources(getSources(), indegree)
}

但是,这不起作用,因为它会导致

  • 列表(2、4、5、7)
  • 列表(2、4、5)
  • 列表(2、4、7)
  • 列表(2, 4)
  • 列表(2, 7)
  • 列表(2)
  • 列表()

堆栈有问题,但我找不到问题。

相关问题:Tail recursive algorithm for generating all topological orderings in a graph

【问题讨论】:

  • 您在stackoverflow.com/a/55786425/5249621 看到我的堆栈解决方案了吗? (第二个标题为“直接解决方案”)
  • 是的,问题不在于如何去做。我知道我需要模拟堆栈,这就是我所做的。但是,我的实现出了点问题,我无法弄清楚。我在这上面花了几天时间,但我真的不明白问题出在哪里。
  • 您以前的版本将结构作为输入并返回一个结构作为输出。现在这些版本返回Unit(通常很难解释副作用)。通常,一个帧包含递归调用的输入参数和结果参数。即使结果是Unit,也应该清楚来自递归子调用的Units 是否已经计算或仍在处理中。
  • 目前,返回类型是 Unit,因为我会在遇到结果时打印结果。我可以很容易地添加一个额外的参数来累积结果。但是,为了调试,这更容易,一旦成功,我可以添加参数。通常,当 sources 参数为 nil 时,会执行递归子调用。
  • 我的意思是计算子调用的帧和未计算子调用的帧应该是不同的帧。在 AndreyTyukin 的回答中是 todos,在我的回答中是 NotExpanded/Expanded/Calculated。看来你没看。

标签: scala recursion tail-recursion topological-sort


【解决方案1】:

这个解决方案是尾递归 AFAICT,并且在我运行它时可以工作,尽管我将它的部分更改回第一个版本,特别是将某些类型从 List 更改为 Set,以保持更改从原来的小(我相信再次将其改回List应该是相对简单的):

def allTopologicalSortsNew[T](graph: Graph[T, DiEdge]): Unit = { 
  type Order = List[graph.NodeT]
  case class Frame(sources: Set[graph.NodeT], indegrees: Map[graph.NodeT, Int], topOrder: Order, cnt: Int)
  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(initialSources: Set[graph.NodeT], initialIndegrees: Map[graph.NodeT, Int]): Unit = {

    def step(stack: List[Frame]): Unit = {
      stack match {
        case Frame(sources, indegrees, topOrder, cnt) :: tail if !sources.isEmpty => {

          val futureFrames = for (src <- sources) yield {
            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
              }
            }

            Frame(newSources, newIndegrees, newTopOrder, cnt + 1)
          }

          step(futureFrames.toList ::: tail)
        }
        case Frame(sources, indegrees, topOrder, cnt) :: tail if sources.isEmpty => {
          println(topOrder.reverse)
          step(tail)
        }
        case Nil =>
      }
    }

    step(List(Frame(initialSources, initialIndegrees, List[graph.NodeT](), 0)))
  }

  processSources(getSources(), indegree)
}

【讨论】:

    【解决方案2】:

    试试

      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))
    
        case class Frame(arg: Argument, parentArg: Option[Argument], res: Result)
    
        case class Argument(sources: Set[graph.NodeT], indegrees: Map[graph.NodeT, Int], topOrder: List[graph.NodeT], cnt: Int)
    
        sealed trait Result
        case object NotExpanded extends Result
        case object Expanded extends Result
        case class Calculated(value: Output) extends Result
    
        type Output = Stream[List[graph.NodeT]]
    
        // extract result from final state of stack
        def processSources(arg: Argument): Output =
          step1(List(Frame(arg, None, NotExpanded))) match {
            case Frame(`arg`, None, Calculated(res)) :: Nil => res
          }
    
        @tailrec
        // process stack as long as necessary
        def step1(stack: List[Frame]): List[Frame] = {
          val x = step(stack, Nil)
          x match {
            case Frame(arg, None, Calculated(res)) :: Nil => x
            case _ => step1(x)
          }
        }
    
        // helper method for handling "recursion backward" case in "step"
        def calcFromChildren(stack: List[Frame], parentArg: Argument): Option[(List[Frame], Frame)] = {
          val (childFrames, rest) = stack.span {
            case Frame(_, Some(`parentArg`), Calculated(_)) => true
            case _ => false
          }
    
          val output: Output = childFrames.map {
            case Frame(arg, Some(`parentArg`), Calculated(res)) => res
          }.toStream.flatten
    
          rest match {
            case Frame(`parentArg`, parentArg1, Expanded) :: rest1 if parentArg.sources.nonEmpty =>
              Some(rest1, Frame(parentArg, parentArg1, Calculated(output)))
            case _ => None
          }
        }
    
        @tailrec
        // process stack once
        def step(stack: List[Frame], acc: List[Frame]): List[Frame] = {
          stack match {
              // recursion backward
            case Frame(arg, Some(parentArg), Calculated(res)) :: frames if calcFromChildren(stack, parentArg).isDefined =>
              val (rest1, parentFrame) = calcFromChildren(stack, parentArg).get
              step(rest1, parentFrame :: acc)
    
              // base
            case Frame(arg, parentArg, _) :: frames if arg.sources.isEmpty && arg.cnt != graph.nodes.size =>
              throw new Error("There is a cycle in the graph.")
            case Frame(arg, parentArg, _) :: frames if arg.sources.isEmpty =>
              val res = arg.topOrder.reverse #:: Stream.empty[List[graph.NodeT]]
              step(frames, Frame(arg, parentArg, Calculated(res)) :: acc)
    
              // recursion forward
            case Frame(arg, parentArg, NotExpanded) :: frames =>
    
              val childFrames = arg.sources.toList.map(src => {
                val newTopOrder = src :: arg.topOrder
                var newSources = arg.sources - src
    
                var newIndegrees = arg.indegrees
                for (adjacent <- src.diSuccessors) {
                  val newIndeg = newIndegrees.get(adjacent).get - 1
                  newIndegrees = newIndegrees.updated(adjacent, newIndeg)
                  if (newIndeg == 0) {
                    newSources = newSources + adjacent
                  }
                }
    
                val newArg = Argument(newSources, newIndegrees, newTopOrder, arg.cnt + 1)
                Frame(newArg, Some(arg), NotExpanded)
              })
    
              step(frames, Frame(arg, parentArg, Expanded) :: childFrames.reverse ::: acc)
    
              // ignore if not "recursion backward" case
            case Frame(arg, parentArg, Expanded) :: frames => step(frames, Frame(arg, parentArg, Expanded) :: acc)
            case Frame(arg, parentArg, Calculated(res)) :: frames => step(frames, Frame(arg, parentArg, Calculated(res)) :: acc)
    
              // stack is processed once
            case Nil => acc.reverse
          }
        }
    
        processSources(Argument(getSources(), indegree, List[graph.NodeT](), 0))
      }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-04
      • 1970-01-01
      • 2013-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多