Scala图形循环检测需要Algo'返回'吗?

时间:2011-09-13 11:16:53

标签: scala detection cycle

我在Scala中为DAG实现了一个小周期检测算法。 “回归”困扰着我 - 我想要一个没有回报的版本......可能吗?

  def isCyclic() : Boolean = {
    lock.readLock().lock()
    try {
      nodes.foreach(node => node.marker = 1)
      nodes.foreach(node => {if (1 == node.marker && visit(node)) return true})
    } finally {
      lock.readLock().unlock()
    }
    false
  }

  private def visit(node: MyNode): Boolean = {
    node.marker = 3

    val nodeId = node.id
    val children = vertexMap.getChildren(nodeId).toList.map(nodeId => id2nodeMap(nodeId))
    children.foreach(child => {
      if (3 == child.marker || (1 == child.marker && visit(child))) return true
    })

    node.marker = 2

    false
  }

5 个答案:

答案 0 :(得分:3)

是的,使用'.find'代替'foreach'+'return':

http://www.scala-lang.org/api/current/index.html#scala.collection.immutable.Seq

def isCyclic() : Boolean = {
  def visit(node: MyNode): Boolean = {
      node.marker = 3

      val nodeId = node.id
      val children = vertexMap.getChildren(nodeId).toList.map(nodeId => id2nodeMap(nodeId))
      val found = children.exists(child => (3 == child.marker || (1 == child.marker && visit(child))))

      node.marker = 2

      found
  }

  lock.readLock().lock()
  try {
    nodes.foreach(node => node.marker = 1)
    nodes.exists(node => node.marker && visit(node))
  } finally {
    lock.readLock().unlock()
  }
}

答案 1 :(得分:2)

我认为可以在不使用标记字段更改节点状态的情况下解决问题。以下是我认为isCyclic应该是什么样子的粗略代码。我目前正在存储被访问的节点对象,如果节点根据节点ID没有相等性,则可以存储节点ID。

def isCyclic() : Boolean = nodes.exists(hasCycle(_, HashSet()))

def hasCycle(node:Node, visited:Seq[Node]) = visited.contains(node) || children(node).exists(hasCycle(_,  node +: visited))


def children(node:Node) = vertexMap.getChildren(node.id).toList.map(nodeId => id2nodeMap(nodeId))

答案 2 :(得分:2)

<强>要点:
我发起了两个解决方案作为通用FP函数,它检测有向图中的周期。根据您隐含的偏好,使用早期return来逃避递归函数已被消除。第一个isCyclic只在DFS(深度优先搜索)重复节点访问时返回一个布尔值。第二个filterToJustCycles返回输入Map的副本,该副本仅过滤到任何/所有周期中涉及的节点,并且在没有找到周期时返回空Map

<强>详细信息:
对于以下内容,请考虑如下编码的有向图:

val directedGraphWithCyclesA: Map[String, Set[String]] =
  Map(
      "A" -> Set("B", "E", "J")
    , "B" -> Set("E", "F")
    , "C" -> Set("I", "G")
    , "D" -> Set("G", "L")
    , "E" -> Set("H")
    , "F" -> Set("G")
    , "G" -> Set("L")
    , "H" -> Set("J", "K")
    , "I" -> Set("K", "L")
    , "J" -> Set("B")
    , "K" -> Set("B")
  )

在下面的两个函数中,类型参数N指的是您要提供的任何“节点”类型。重要的是,提供的“Node”类型都是不可变的,并且具有稳定的equalshashCode实现(所有这些实现都是使用不可变的case类自动发生的。)

第一个函数isCyclic在本质上与@ the-archetypal-paul提供的解决方案版本类似。它假定有向图已转换为Map[N, Set[N]],其中N是图中节点的标识。

如果您需要了解如何将自定义有向图实现一般转换为Map[N, Set[N]],我已经在本答案的最后部分概述了一个通用的解决方案。

如此调用isCyclic函数:

val isCyclicResult = isCyclic(directedGraphWithCyclesA)

将返回:

`true`

未提供进一步资料。并且在检测到对节点的第一次重复访问时中止DFS(深度优先搜索)。

def isCyclic[N](nsByN: Map[N, Set[N]]) : Boolean = {
  def hasCycle(nAndNs: (N, Set[N]), visited: Set[N] = Set[N]()): Boolean =
    if (visited.contains(nAndNs._1))
      true
    else
      nAndNs._2.exists(
        n =>
          nsByN.get(n) match {
            case Some(ns) =>
              hasCycle((n, ns), visited + nAndNs._1)
            case None =>
              false
          }
      )
  nsByN.exists(hasCycle(_))
}

第二个函数filterToJustCycles使用集合缩减技术递归过滤掉Map中未引用的根节点。如果提供的节点图中没有循环,则返回的.isEmpty上的true将为Map。但是,如果有任何循环,则返回参与任何循环所需的所有节点,并过滤掉所有其他非循环参与节点。

同样,如果您需要了解如何将自定义有向图实现一般转换为Map[N, Set[N]],我已经在本答案的最后部分概述了一个通用的解决方案。

如此调用filterToJustCycles函数:

val cycles = filterToJustCycles(directedGraphWithCyclesA)

将返回:

Map(E -> Set(H), J -> Set(B), B -> Set(E), H -> Set(J, K), K -> Set(B))

然后在此Map之间创建遍历,以通过其余节点生成任何或所有各种循环路径,这是微不足道的。

def filterToJustCycles[N](nsByN: Map[N, Set[N]]): Map[N, Set[N]] = {
  def recursive(nsByNRemaining: Map[N, Set[N]], referencedRootNs: Set[N] = Set[N]()): Map[N, Set[N]] = {
    val (referencedRootNsNew, nsByNRemainingNew) = {
      val referencedRootNsNewTemp =
        nsByNRemaining.values.flatten.toSet.intersect(nsByNRemaining.keySet)
      (
          referencedRootNsNewTemp
        , nsByNRemaining.collect {
            case (t, ts) if referencedRootNsNewTemp.contains(t) && referencedRootNsNewTemp.intersect(ts.toSet).nonEmpty =>
              (t, referencedRootNsNewTemp.intersect(ts.toSet))
          }
        )
    }
    if (referencedRootNsNew == referencedRootNs)
      nsByNRemainingNew
    else
      recursive(nsByNRemainingNew, referencedRootNsNew)
  }
  recursive(nsByN)
}

那么,如何将自定义有向图实现一般转换为Map[N, Set[N]]

本质上,“Go Scala案例类!”

首先,让我们在预先存在的有向图中定义一个真实节点的示例:

class CustomNode (
    val equipmentIdAndType: String //"A387.Structure" - identity is embedded in a string and must be parsed out
  , val childrenNodes: List[CustomNode] //even through Set is implied, for whatever reason this implementation used List
  , val otherImplementationNoise: Option[Any] = None
)

同样,这只是一个例子。您可能涉及子类化,委托等。目的是访问能够获取两个基本事物的东西以使其工作:

  • 节点的身份;即,要区分它并使其与同一有向图中的所有其他节点不同,
  • 特定节点的直接子节点的标识集合 - 如果特定节点没有任何子节点,则此集合将为空

接下来,我们定义一个帮助对象DirectedGraph,它将包含转换的基础结构:

  • Node:一个包装CustomNode
  • 的适配器特征
  • toMap:一个将List[CustomNode]转换为Map[Node, Set[Node]]的函数(其类型相当于我们的Map[N, Set[N]]目标类型)

以下是代码:

object DirectedGraph {
  trait Node[S, I] {
    def source: S
    def identity: I
    def children: Set[I]
  }

  def toMap[S, I, N <: Node[S, I]](ss: List[S], transformSToN: S => N): Map[N, Set[N]] = {
    val (ns, nByI) = {
      val iAndNs =
        ss.map(
          s => {
            val n =
              transformSToN(s)
            (n.identity, n)
          }
        )
      (iAndNs.map(_._2), iAndNs.toMap)
    }
    ns.map(n => (n, n.children.map(nByI(_)))).toMap
  }
}

现在,我们必须生成实际的适配器CustomNodeAdapter,它将包装每个CustomNode实例。此适配器以非常特定的方式使用案例类;即指定两个构造函数参数列表。它确保案例类符合Set要求Set成员具有正确equalshashCode实现的要求。有关为何以及如何以这种方式使用案例类的详细信息,请参阅此StackOverflow question and answer

object CustomNodeAdapter extends (CustomNode => CustomNodeAdapter) {
  def apply(customNode: CustomNode): CustomNodeAdapter =
    new CustomNodeAdapter(fetchIdentity(customNode))(customNode) {}

  def fetchIdentity(customNode: CustomNode): String =
    fetchIdentity(customNode.equipmentIdAndType)

  def fetchIdentity(eiat: String): String =
    eiat.takeWhile(char => char.isLetter || char.isDigit)
}
abstract case class CustomNodeAdapter(identity: String)(customNode: CustomNode) extends DirectedGraph.Node[CustomNode, String] {
  val children =
    customNode.childrenNodes.map(CustomNodeAdapter.fetchIdentity).toSet
  val source =
    customNode
}

我们现在已经建立了基础设施。让我们定义一个由CustomNode组成的“真实世界”有向图:

val customNodeDirectedGraphWithCyclesA =
  List(
      new CustomNode("A.x", List("B.a", "E.a", "J.a"))
    , new CustomNode("B.x", List("E.b", "F.b"))
    , new CustomNode("C.x", List("I.c", "G.c"))
    , new CustomNode("D.x", List("G.d", "L.d"))
    , new CustomNode("E.x", List("H.e"))
    , new CustomNode("F.x", List("G.f"))
    , new CustomNode("G.x", List("L.g"))
    , new CustomNode("H.x", List("J.h", "K.h"))
    , new CustomNode("I.x", List("K.i", "L.i"))
    , new CustomNode("J.x", List("B.j"))
    , new CustomNode("K.x", List("B.k"))
    , new CustomNode("L.x", Nil)
  )

最后,我们现在可以进行如下转换:

val transformCustomNodeDirectedGraphWithCyclesA =
  DirectedGraph.toMap[CustomNode, String, CustomNodeAdapter](customNodes1, customNode => CustomNodeAdapter(customNode))

我们可以使用类型为transformCustomNodeDirectedGraphWithCyclesA的{​​{1}},并将其提交给两个原始函数。

如此调用Map[CustomNodeAdapter,Set[CustomNodeAdapter]]函数:

isCyclic

将返回:

val isCyclicResult = isCyclic(transformCustomNodeDirectedGraphWithCyclesA)

如此调用`true` 函数:

filterToJustCycles

将返回:

val cycles = filterToJustCycles(transformCustomNodeDirectedGraphWithCyclesA)

如果需要,可以将此Map( CustomNodeAdapter(B) -> Set(CustomNodeAdapter(E)) , CustomNodeAdapter(E) -> Set(CustomNodeAdapter(H)) , CustomNodeAdapter(H) -> Set(CustomNodeAdapter(J), CustomNodeAdapter(K)) , CustomNodeAdapter(J) -> Set(CustomNodeAdapter(B)) , CustomNodeAdapter(K) -> Set(CustomNodeAdapter(B)) ) 转换回Map

Map[CustomNode, List[CustomNode]]

如果您有任何问题,疑问或疑虑,请告诉我,我会尽快解决。

答案 3 :(得分:0)

如果p = node =&gt; node.marker == 1&amp;&amp;访问(节点)并假设节点是一个列表,您可以选择以下任何一个:

  • nodes.filter(p).length>0
  • nodes.count(p)>0
  • nodes.exists(p)(我认为最相关)

我不确定每种方法的相对复杂性,并希望得到社区其他成员的评论

答案 4 :(得分:0)

添加的答案只是为了表明变异 - visited也不是太难以理解(尽管未经测试!)

def isCyclic() : Boolean =
{
     var visited = HashSet()

     def hasCycle(node:Node) = {
        if (visited.contains(node)) {
           true
        } else {
           visited :+= node
           children(node).exists(hasCycle(_))
        }
    }
    nodes.exists(hasCycle(_))
}

def children(node:Node) = vertexMap.getChildren(node.id).toList.map(nodeId => id2nodeMap(nodeId))