Scala Graph Cycle Detection Algo «возврат» нужен? - PullRequest
2 голосов
/ 13 сентября 2011

Я реализовал алгоритм обнаружения малого цикла для DAG в Scala. «Возврат» беспокоит меня - я хотел бы иметь версию без возврата ... возможно?

  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 ]

3 голосов
/ 13 сентября 2011

Да, используя «.find» вместо «foreach» + «return»:

http://www.scala -lang.org / api / current / index.html # scala.collection.неизменный. 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()
  }
}
2 голосов
/ 22 марта 2016

Резюме:
Я создал два решения как универсальные функции 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, использует метод сокращения набора для рекурсивной фильтрации корневых узлов без ссылок на карте. Если в предоставленном графе узлов нет циклов, то .isEmpty будет возвращено 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 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.Этот адаптер использует класс case очень специфическим образом;т.е. указание двух списков параметров конструктора.Это гарантирует, что класс case соответствует Set требованию, чтобы Set член имел правильные реализации equals и hashCode.Подробнее о том, почему и как использовать класс case таким образом, см. Этот вопрос и ответ StackOverflow :

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, который имеет тип 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]]:

cycles.map {
  case (customNodeAdapter, customNodeAdapterChildren) =>
    (customNodeAdapter.source, customNodeAdapterChildren.toList.map(_.source))
}

Если у вас есть какие-либо вопросы, проблемы или проблемы, пожалуйста, дайте мне знать, и я рассмотрю их как можно скорее.

2 голосов
/ 13 сентября 2011

Я думаю, что проблему можно решить без изменения состояния узла с помощью поля маркера. Ниже приведен грубый код того, что, по моему мнению, должно выглядеть isCyclic. В настоящее время я храню объекты узлов, которые посещаются, вместо этого вы можете сохранить идентификаторы узлов, если узел не имеет равенства на основе идентификатора узла.

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))
0 голосов
/ 13 сентября 2011

Ответ добавлен только для того, чтобы показать, что изменяемый файл 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))
0 голосов
/ 13 сентября 2011

Если p = node => node.marker == 1 && визит (узел) и предполагается, что узлы - это список, вы можете выбрать любой из следующих вариантов:

  • nodes.filter(p).length>0
  • nodes.count(p)>0
  • nodes.exists(p) (я думаю, что наиболее актуально)

Я не уверен в относительной сложности каждого метода и был бы признателен за комментарий от других членовсообщество

...