Как генерировать транзитивное замыкание множества кортежей?

каков наилучший способ генерации транзитивного замыкания набора кортежей?

пример:

  • вход Set((1, 2), (2, 3), (3, 4), (5, 0))
  • выход Set((1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4), (5, 0))

4 ответов


//one transitive step
def addTransitive[A, B](s: Set[(A, B)]) = {
  s ++ (for ((x1, y1) <- s; (x2, y2) <- s if y1 == x2) yield (x1, y2))
}

//repeat until we don't get a bigger set
def transitiveClosure[A,B](s:Set[(A,B)]):Set[(A,B)] = {
  val t = addTransitive(s)
  if (t.size == s.size) s else transitiveClosure(t)
}

println(transitiveClosure(Set((1,2), (2,3), (3,4))))

Это не очень эффективная реализация, но это просто.


С помощью unfold,

def unfoldRight[A, B](seed: B)(f: B => Option[(A, B)]): List[A] = f(seed) match {
  case Some((a, b)) => a :: unfoldRight(b)(f)
  case None => Nil
}

def unfoldLeft[A, B](seed: B)(f: B => Option[(B, A)]) = {
  def loop(seed: B)(ls: List[A]): List[A] = f(seed) match {
    case Some((b, a)) => loop(b)(a :: ls)
    case None => ls
  }

  loop(seed)(Nil)
}

становится довольно просто:

def transitiveClosure(input: Set[(Int, Int)]) = {
    val map = input.toMap
    def closure(seed: Int) = unfoldLeft(map get seed) {
        case Some(`seed`) => None
        case Some(n)      => Some(seed -> n -> (map get n))
        case _            => None
    }
    map.keySet flatMap closure
}

другой способ написания closure это:

def closure(seed: Int) = unfoldRight(seed) {
    case n if map.get(n) != seed => map get n map (x => seed -> x -> x)
    case _ => None
}

Я сам не уверен, какой путь мне больше нравится. Мне нравится элегантность тестирования для Some(seed) чтобы избежать циклов, но, по той же причине, мне также нравится элегантность отображения результата map get n.

ни одна версия не возвращается seed -> seed для циклов, поэтому вам придется добавить это, если это необходимо. Здесь:

    def closure(seed: Int) = unfoldRight(map get seed) {
        case Some(`seed`) => Some(seed -> seed -> None)
        case Some(n)      => Some(seed -> n -> (map get n))
        case _            => None
    }

моделируйте задачу как направленный граф следующим образом:

представляют числа в кортежах как вершины в графе. Тогда каждый кортеж (x, y) представляет собой направленное ребро от x до y. После этого используйте Warshall это найти транзитивное замыкание графа.

для результирующего графика каждое направленное ребро затем преобразуется в кортеж (x, y). Это транзитивное замыкание множества кортежей.


предполагая, что у вас есть DAG (в ваших данных примера нет циклов), вы можете использовать приведенный ниже код. Он ожидает, что DAG как карта от T до List[T], которую вы можете получить от своего ввода с помощью

input.groupBy(_._1) mapValues ( _ map (_._2) )

вот транзитивное замыкание:

def transitiveClosure[T]( dag: Map[ T, List[T] ] ) = {
  var tc = Map.empty[ T, List[T] ]
  def getItemTC( item:T ): List[T] = tc.get(item) match {
    case None =>
      val itemTC = dag(item) flatMap ( x => x::getItemTC(x) )
      tc += ( item -> itemTC )
      itemTC
    case Some(itemTC) => itemTC
  }
  dag.keys foreach getItemTC
  tc
}

этот код определяет закрытие для каждого элемента только один раз. Однако:

  • этот код может вызвать переполнение стека, если есть достаточно длинные пути через DAG (рекурсия не хвостовая рекурсия).
  • для большого графика вам, вероятно, будет лучше сделать TC изменяемой картой, а затем преобразовать ее в конце, если вы хотите неизменяемую карту.
  • если бы ваши элементы были действительно маленькими целыми числами, как в вашем примере, вы могли бы значительно повысить производительность, используя массивы, а не карты, хотя это усложнило бы некоторые вещи.

чтобы устранить проблему переполнения стека (для DAG), вы можете выполнить топологическую сортировку, переверните его и обработайте элементы по порядку. Но Смотрите также эту страницу:

лучший известный алгоритм транзитивного замыкания для графа