我在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
}
答案 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”类型都是不可变的,并且具有稳定的equals
和hashCode
实现(所有这些实现都是使用不可变的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
成员具有正确equals
和hashCode
实现的要求。有关为何以及如何以这种方式使用案例类的详细信息,请参阅此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))