Топологическая сортировка в OCaml - PullRequest
20 голосов
/ 11 января 2011

Я пытаюсь написать топологическую сортировку в ocaml, но я новичок (в алгоритмах OCaml и графиков), и я не могу сделать это самостоятельно.

Мне легче думать отопологическая сортировка, например, в C ++ (и в Интернете много примеров топологической сортировки в C ++), но я хочу изучить что-то новое.Более того, я нашел несколько примеров топологической сортировки, написанных на OCaml, но, честно говоря, я их не понимаю.

Допустим, у меня есть список (int * int list) list, например:

myList = [(1, [2]); (5, [6; 7]); (3, [2]); (6, [3; 7]); (8, [7]); (4, [3; 1])];;

, а это означает, что мне нужно «выполнить» задачу 1 перед задачей 2, задачу 4 перед задачами 3 и 1 и т. Д.

Я думаю, что вывод для этого списка должен быть:

[8; 5; 6; 7; 4; 3; 1; 2]

(однако я не уверен, потому что я только что сделал этот пример, так что если я ошибаюсь, поправьте меня, пожалуйста)

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

AFAIK, мне нужно использовать DFS в алгоритме для топологической сортировки, который (DFS) я не знаю, как реализовать в OCaml (я понимаю основную идею, но я не чувствую , как это работает в OCaml / функциональном программировании).

Буду очень признателен за вашу помощьЧтобы понять меня, это понятия (я имею в виду топологический вид, DFS в OCaml / функциональное программирование).Если бы Вы могли, было бы здорово, если бы Ты показал мне примеры кодов, потому что чтение кода - это (для меня) лучший способ понять концепцию алгоритма.

Я знаю, что для большинства из Вас это простовопрос, но я надеюсь, что это не помешает Вам помочь мне.

PS: Я изучаю OCaml самостоятельно (я учусь в средней школе), поэтому у меня нет твердой теориифон (либо в OCaml, либо в алгоритмах).Я начал изучать OCaml, потому что хотел понять концепцию рекурсии, и теперь этот язык кажется интересным, потому что он действительно отличается от C ++, поэтому я все еще пытаюсь изучить что-то новое в OCaml.

Ответы [ 4 ]

19 голосов
/ 11 января 2011

Во-первых, имейте в виду, что Objective Caml поддерживает стиль программирования, который, несмотря на синтаксические различия, довольно похож на C ++ посредством изменяемых структур данных (ссылки, массивы, хеш-таблицы) и императивных конструкций (для циклов for и while, присвоение переменной). Ниже я предполагаю, что вы на самом деле пытаетесь написать свой топологический вид в идиоматическом чисто функциональном стиле.

Чистое функциональное программирование в основном декларативное: эта функция применяется к этому значению , это это другое значение. Вот почему правая часть let x = является выражением (оценивается как значение), а не последовательностью действий, которая использует return. Конечно, возникают проблемы при адаптации алгоритма, который обычно описывается как последовательность шагов.

К счастью, есть шаблон (на самом деле, семейство шаблонов), который позволяет вам представлять алгоритмы императивного вывода в функциональном стиле, поворачивая «изменить значение X» на «возвращая новый объект, который идентичен старому, за исключением значения Х ".

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

Используя вышеописанную структуру графа в сочетании с представлением списка для двух наборов (вряд ли это оптимально - Set будет лучшим выбором здесь), алгоритм будет выглядеть примерно так:

let dfs graph start_node = 
  let rec explore path visited node = 
    if List.mem node path    then raise (CycleFound path) else
    if List.mem node visited then visited else     
      let new_path = node :: path in 
      let edges    = List.assoc node graph in
      let visited  = List.fold_left (explore new_path) visited edges in
      node :: visited
  in explore [] [] start_node

Три важные детали выше: во-первых, DFS говорит, что, закончив изучение всех дочерних узлов данного узла, вы должны удалить этот узел из списка «текущий путь» и поместить его в список «посещенных». Поскольку мы используем неизменные структуры данных, первый шаг не требуется: его единственная цель состояла в том, чтобы отменить вставку узла, когда началось исследование, и в нашей версии список new_path не изменяется рекурсивным вызовом explore. Это пример случая, когда с функциональными структурами данных работать удобнее, чем с императивными структурами.

Еще одной важной деталью является использование List.fold_left. Когда мы начали делать состояние явным, мы заменили неявные императивные функции типа -> unit на явные функции типа -> state -> .. -> state (примите состояние в качестве параметра, верните новое состояние). Итак, императивный список исследований, который выглядел так:

f edge_1 ; f edge_2 ; f edge_3

Теперь выглядит так:

let state = f (f (f state edge_1) edge_2) edge_3)

Именно это и делает List.fold_left f state [edge_1 ; edge_2 ; edge_3]. У моего собственного рога, но у меня есть хорошая статья об этом здесь .

Третий момент заключается в том, что операция «добавить элемент в набор» при использовании списков для представления наборов записывается просто как element :: set, поскольку это операция, которая возвращает новый набор (список), который содержит все элементы оригинального набора вместе с новым элементом. Это оставляет исходный набор нетронутым (что хорошо по причинам, описанным в первом шаге) при использовании постоянного объема памяти (он создает ячейку cons - простую пару голова-хвост, содержащую ссылку на элемент и ссылку на набор ): вы не только получаете возможность отмены, но и делаете это без дополнительных затрат.

Приведенный выше алгоритм «вставляет» узлы в visited, начиная с листьев исследования DFS, которые в вашем случае представляют те узлы, которые должны быть выполнены последними. Короче говоря, возвращаемый список отсортирован топологически, но может содержать не все элементы, поскольку отправная точка может быть не единственным корневым элементом (или вообще не быть корневым элементом). Таким образом, здесь есть дополнительный этап обработки, заключающийся в том, чтобы убрать еще один узел из графа, пока весь граф не будет исследован.

Или, другими словами, запуск нового исследования DFS с каждого узла в графике , но игнорирование любых ранее исследованных узлов - что эквивалентно сохранению списка посещенных элементов от одного исследования DFS до следующего.

Используя небольшую настройку нашего предыдущего алгоритма, это займет всего две строки:

let dfs graph visited start_node = 
  let rec explore path visited node = 
    if List.mem node path    then raise (CycleFound path) else
    if List.mem node visited then visited else     
      let new_path = node :: path in 
      let edges    = List.assoc node graph in
      let visited  = List.fold_left (explore new_path) visited edges in
      node :: visited
  in explore [] visited start_node

let toposort graph = 
  List.fold_left (fun visited (node,_) -> dfs graph visited node) [] graph

Подстройка состоит в том, чтобы позволить вызывающему dfs указать список уже посещенных узлов. Перенос списка посещенных узлов при запуске DFS с каждого узла выполняется с использованием List.fold_left точно так же, как и раньше.

EDIT : кроме типа исключения, здесь нет ничего, что ограничивало бы тип элементов в графе. Однако исключение не может быть полиморфным, поэтому у вас есть два возможных решения. Во-первых, нужно отказаться от фактического возврата любых данных, за исключением:

exception CycleFound

... raise CycleFound ...

Это вернет тип toposort обратно к более общему ('a * ('a list)) list -> 'a list.

Другое решение - довольно продвинутый OCaml: вам нужно сделать модуль , который содержит исключение и топологическую сортировку полиморфной в этом конкретном типе, следующим образом:

module type NODE = sig
  type t 
end

module type Topo = functor (Node:NODE) -> struct 
  exception CycleFound of Node.t list      
  let dfs ...
  let sort ...  
end

При этом тип Topo(Node).sort будет (Node.t * (Node.t list)) list -> Node.t list, что означает, что вы можете отсортировать любой тип по вашему желанию, определив модуль узла с этим типом:

type recipe = Eggs | Milk | Wheat | Mix | Cook | Serve

module Node = struct 
  type t = recipe 
end

let graph = [ Wheat, [Eggs,Milk,Mix] ;
              Milk,  [Mix] ;
              Eggs,  [Mix] ;
              Mix,   [Cook] ;
              Cook,  [Serve] ;
              Serve, [] ]

module RecipeTopo = Topo(Node)

let sorted = RecipeTopo.sort graph
4 голосов
/ 11 января 2011

[В случае, если вы не знаете термин, где я пишу DAG ниже, я имею в виду «ориентированный ациклический граф» или набор из / в ребер, соединяющих вершины, так что циклов нет.]

Один из способов сделать это состоит в том, чтобы расширить частичный порядок (структуру DAG) в общий порядок (поэтому для каждой пары различных вершин u и v либо u является преемником v, либо наоборот).Затем вы можете отсортировать вершины по порядку: u предшествует v, если v является преемником u.

Вы можете построить общий порядок, начав с пустого графа и добавив по одному ребру за раз из исходного DAG,То есть элемент (u, [v1; v2; ...; vn]) в исходном DAG соответствует ребрам (u, v1), (u, v2), ..., (u, vn).Для каждого такого ребра (u, v) найдите предшественников P of u и преемников S of v из вашего общего заказа.Затем добавьте (p, s) к вашему суммарному заказу для всех p в PU {u} и s в SU {v}.

СЕЙЧАС!Более быстрый способ сделать это - найти корень в исходной группе обеспечения доступности баз данных (т. Е. Вершину без предшественников) и выполнить поиск в глубину из этого корня, гарантируя, что вы никогда не посетите одну и ту же вершину дважды.Каждый раз, когда вы возвращаетесь назад, вы «выводите» вершину, которую вы покидаете.Таким образом, вы создаете топологический вид вашего DAG.Если остались какие-либо вершины, промойте их и повторяйте, пока все не будет сделано.

0 голосов
/ 11 января 2011

Сначала вы должны попробовать DFS, это проще и выгоднее.

0 голосов
/ 11 января 2011

Я не знаю OCaml, но в Википедии , аккредитованном при Кане, есть простой алгоритм, который я успешно использовал (расшифровка для Python).Это довольно просто, так что, возможно, вы могли бы перевести его на OCaml.Вот оно:

L ← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edges
while S is non-empty do
    remove a node n from S
    insert n into L
    for each node m with an edge e from n to m do
        remove edge e from the graph
        if m has no other incoming edges then
            insert m into S
if graph has edges then
    output error message (graph has at least one cycle)
else 
    output message (proposed topologically sorted order: L)
...