Функциональная реализация алгоритма сильно связанных компонентов Tarjan

Я пошел дальше и реализовал версию учебника по алгоритму Tarjan SCC в Scala. Однако мне не нравится код - он очень необходим/процедурный с множеством мутирующих состояний и индексами бухгалтерского учета. Существует ли более "функциональная" версия алгоритма? Я считаю, что императивные версии алгоритмов скрывают основные идеи, стоящие за алгоритмом, в отличие от функциональных версий. Я нашел кого-то другого, столкнувшегося с той же проблемой с этим конкретным алгоритмом, но я не смог перевести его код Clojure в idomatic Scala.

Примечание. Если кто-то хочет экспериментировать, у меня хорошая настройка, которая генерирует случайные графики, а проверяет ваш алгоритм SCC и запускает Floyd-Warshall

Ответы

Ответ 1

Следующий функциональный код Scala генерирует карту, которая назначает представителя каждому node графика. Каждый представитель идентифицирует один сильно связанный компонент. Код основан на алгоритме Тарьяна для сильно связанных компонентов.

Чтобы понять алгоритм, достаточно было бы понять сгиб и контракт функции dfs.

def scc[T](graph:Map[T,Set[T]]): Map[T,T] = {
  //`dfs` finds all strongly connected components below `node`
  //`path` holds the the depth for all nodes above the current one
  //'sccs' holds the representatives found so far; the accumulator
  def dfs(node: T, path: Map[T,Int], sccs: Map[T,T]): Map[T,T] = {
    //returns the earliest encountered node of both arguments
    //for the case both aren't on the path, `old` is returned
    def shallowerNode(old: T,candidate: T): T = 
      (path.get(old),path.get(candidate)) match {
        case (_,None) => old
        case (None,_) => candidate
        case (Some(dOld),Some(dCand)) =>  if(dCand < dOld) candidate else old
      }

    //handle the child nodes
    val children: Set[T] = graph(node)
    //the initially known shallowest back-link is `node` itself
    val (newState,shallowestBackNode) = children.foldLeft((sccs,node)){
      case ((foldedSCCs,shallowest),child) =>
        if(path.contains(child))
          (foldedSCCs, shallowerNode(shallowest,child))
        else {
          val sccWithChildData = dfs(child,path + (node -> path.size),foldedSCCs)
          val shallowestForChild = sccWithChildData(child)
          (sccWithChildData, shallowerNode(shallowest, shallowestForChild))
        }
    }

    newState + (node -> shallowestBackNode)
  }

  //run the above function, so every node gets visited
  graph.keys.foldLeft(Map[T,T]()){ case (sccs,nextNode) =>
    if(sccs.contains(nextNode))
      sccs
    else
      dfs(nextNode,Map(),sccs)
  }
}

Я тестировал код только на графике примера, найденном на странице Википедии.

Разница с императивной версией

В отличие от исходной реализации, моя версия позволяет явно разматывать стек и просто использует правильную (не хвостовую) рекурсивную функцию. Стек представляет собой постоянную карту, называемую path. В моей первой версии я использовал List как стек; но это было менее эффективно, так как нужно было искать элементы, содержащие элементы.

Эффективность

Код довольно эффективен. Для каждого ребра вам необходимо обновить и/или получить доступ к неизменяемой карте path, которая стоит O(log|N|), в общей сложности O(|E| log|N|). Это контрастирует с O(|E|), достигнутым императивной версией.

Выполнение линейного времени

В докладе Криса Окасаки в линейке Haskell для решения задачи нахождения сильносвязанных компонент дается линейное временное решение. Их реализация основана на алгоритме Косараджу для поиска ГТК, что в основном требует двух первых пересечений по глубине. Основной вклад в документ представляет собой ленивую, линейную временную реализацию DFS в Haskell.

Для достижения линейного решения времени требуется набор с тестом t1 и singleton add и membership. Это в основном та же проблема, что и решение, данное в этом ответе, имеет более высокую сложность, чем императивное решение. Они решают его с помощью потоков состояний в Haskell, что также можно сделать в Scala (см. Scalaz). Поэтому, если кто-то хочет сделать код довольно сложным, можно реализовать алгоритм Tarjan SCC для функциональной версии O(|E|).

Ответ 3

Посмотрите https://github.com/jordanlewis/data.union-find, реализацию Clojure алгоритма. Это сортируется как структура данных, но алгоритм есть все. И это чисто функционально, конечно.