2011-01-11 17 views
20

Estoy tratando de escribir clasificación topológica en ocaml, pero soy un principiante (en OCaml & algoritmos de gráficos) y no puedo hacer esto por mí mismo.Clasificación topológica en OCaml

Es más fácil para mí pensar en la clasificación topológica, por ejemplo, en C++ (y hay muchos ejemplos de clasificación topológica en C++ en Internet), pero quiero aprender algo nuevo. Además, he encontrado algunos ejemplos de clasificación topológica escritos en OCaml, pero no los entiendo, para ser francos.

Digamos que tengo una lista (int * int list) list, por ejemplo:

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

y eso significa, que tengo que "hacer" la tarea 1 antes de tarea 2, tarea 4 antes de tareas 3 y 1 etc.

Creo, que la producción de esta lista debe ser:

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

(sin embargo no estoy seguro, porque acabo de hacer este ejemplo, por lo que si estoy equivocado, por favor corríjanme)

Además, he leído, ese tipo topológico no funciona para ciclos en gráficos, por lo que debe haber algún tipo de condición para los ciclos: cuando el gráfico dado tiene ciclo (s) levantamos la excepción (creo que es una buena idea).

yo sepa, necesito utilizar DFS en el algoritmo de clasificación topológica, que (DFS) No sé cómo poner en práctica en OCaml (entiendo idea principal, pero no me siento , cómo funciona en OCaml/programación funcional).

Realmente agradecería su ayuda para comprenderme estos conceptos (me refiero al tipo topológico, DFS en OCaml/programación funcional). Si puede, sería genial si me muestra códigos de ejemplo, porque la lectura del código es (para mí) el mejor método para comprender el concepto del algoritmo.

Sé que para la mayoría de Ustedes esta es una pregunta simple, pero espero que no le impida ayudarme.

PD: Estoy aprendiendo OCaml por mi cuenta (estoy en una escuela secundaria), así que no tengo una sólida base teórica (ya sea en OCaml o algoritmos). Empecé a aprender OCaml, porque quería entender el concepto de recursión, y ahora este lenguaje parece ser interesante, porque es realmente diferente de C++, así que todavía estoy tratando de aprender algo nuevo en OCaml.

+3

Si su pregunta había sido expresado de otra manera, me he recomendado O simplemente usa http://ocamlgraph.lri.fr/.Sin embargo, cuando esté más cómodo con OCaml y, en particular, con el sistema de módulos de OCaml, le recomiendo que vuelva a visitar este ejemplo. Una buena introducción a Ocamlgraph está en http://alexleighton.wordpress.com/2009/04/22/intro-to-ocamlgraph/. –

Respuesta

4

[En caso de que no conoce el término, donde escribo DAG continuación me refiero a "grafo acíclico dirigido", o una colección de desde/hacia los bordes que conectan los vértices de tal manera que no hay ciclos.]

Una forma de hacerlo es extender su orden parcial (su estructura DAG) en un orden total (por lo que para cada par de vértices distintos uy v, u es un sucesor de v o viceversa). Luego puede ordenar sus vértices en orden: u viene antes de v si v es un sucesor de u.

Puede construir su orden total comenzando con el gráfico vacío y agregando un borde a la vez desde su DAG original. Es decir, un elemento (u, [v1; v2; ...; vn]) en su DAG original corresponde a los bordes (u, v1), (u, v2), ..., (u, vn). Para cada uno de dichos flancos (u, v), encuentre los predecesores P de uy los sucesores S de v de su orden total. Luego agregue (p, s) a su orden total para todo p en P U {u} y s en S U {v}.

AHORA! Una forma más rápida de hacerlo es encontrar una raíz en su DAG original (es decir, un vértice sin predecesores) y hacer una primera búsqueda en profundidad desde esa raíz, asegurando que nunca visite el mismo vértice dos veces. Cada vez que retrocedes en tu recorrido, "sacas" el vértice que estás dejando. De esta forma construyes el tipo topológico de tu DAG. Si quedan vértices, enjuague con espuma y repita hasta que todos estén listos.

+0

Estaba pensando en el método Usted mencionó primero, pero la segunda idea es muy interesante. Gracias;) – justme

+0

Para ser exactos, ni siquiera necesita comenzar sus iteraciones con un nodo raíz: cualquier nodo funcionará, porque los nodos no raíz tienen que salir antes que los nodos raíz de todos modos. –

+0

@Victor - Ah, lo siento: quise agregar que si no puede encontrar una raíz, entonces su gráfica contiene un ciclo. – Rafe

-3

No conozco OCaml, pero hay un algoritmo simple en Wikipedia acreditado en Kahn que he utilizado con éxito (transcribiendo a Python). Es bastante simple, así que quizás podrías traducirlo a OCaml. Aquí está:

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) 
+2

Sí, puedes traducir esto textualmente a OCaml, pero este estilo no es realmente idiomático. Esta versión requiere mutabilidad y ciclos imperativos, básicamente terminas escribiendo C++ en una sintaxis un poco diferente. Estoy seguro de que OP quería algo que funcione con estructuras de datos inmutables y escrito en un estilo más idiomático y funcional. – Juliet

+0

@Juliet: Sí, tienes razón. Debería haber leído la pregunta del OP con más cuidado, especialmente el segundo párrafo. Pero aparte de eso, y al no estar familiarizado con los lenguajes funcionales, parece que un enfoque de procedimiento que utiliza estructuras de datos mutables tiene la ventaja de ser comprensible para el programador promedio. Y en el mundo real ¿no es eso realmente más importante que la capacidad matemática? Sin embargo, como ejercicio mental, estoy de acuerdo en que la programación funcional pura es fascinante y educativa. –

+0

"parece que un enfoque de procedimiento que utiliza estructuras de datos mutables tiene la ventaja de ser comprensible para el programador promedio". Eso es bastante ingenuo. – nlucaroni

-2

Primero debe probar DFS, es más fácil y gratificante.

+1

¿Más fácil que qué? Primero antes de que? Supongo que recompensar es lindo, sin embargo. – Gabe

18

Primero, tenga en cuenta que Objective Caml admite un estilo de programación que, a pesar de las diferencias sintácticas, es bastante similar a C++, mediante estructuras de datos mutables (referencias, matrices, tablas hash) y construcciones imperativas (ciclos for y while , asignación de variable). Asumo a continuación que en realidad estás tratando de escribir tu género topológico en un estilo idiomático funcional puro.

La programación funcional pura es principalmente declarativa: esta función aplicada a ese valor es este otro valor. Esta es la razón por la que el lado derecho de let x = es una expresión (se evalúa como un valor) en lugar de una secuencia de acciones que usa return. Por supuesto, los problemas aparecen cuando se adapta un algoritmo que comúnmente se describe como una serie de pasos.

Afortunadamente, hay un patrón (en realidad, una familia de patrones) que le permite representar algoritmos imperativos en estilo funcional al convertir "cambiar el valor de X" en "devolver un nuevo objeto que es idéntico al anterior" , excepto por el valor de X ".

Un algoritmo DFS tradicional implica recorrer el gráfico mientras recuerda qué elementos ya han sido visitados, en general (para que no los visite más de una vez) y para llegar a su posición actual (para que pueda detectar ciclos). Entonces, el estado imperativo de un algoritmo DFS está compuesto por el nodo actual, el conjunto de nodos visitados y el conjunto de nodos en la ruta actual. Todos estos datos deberán ser provistos a las llamadas recursivas, y cualquier cambio permanente tendrá que ser devuelto por esas mismas llamadas recursivas.

Usando su estructura gráfica de arriba, en combinación con una representación de la lista para los dos conjuntos (que es casi óptima - Set sería una mejor opción aquí), el algoritmo se vería algo como esto:

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 

Tres detalles importantes arriba: primero, DFS dice que cuando haya terminado de explorar todos los elementos secundarios de un nodo determinado, debe eliminar ese nodo de la lista de "ruta actual" y colocarlo en la lista "visitado". Dado que estamos utilizando estructuras de datos inmutables, el primer paso es innecesario: su único propósito era deshacer la inserción del nodo cuando se inició la exploración, y en nuestra versión la lista new_path no es modificada por la llamada recursiva al explore. Este es un ejemplo de caso en que las estructuras de datos funcionales son más cómodas de trabajar que las estructuras imperativas.

Otro detalle importante es el uso de List.fold_left. Cuando comenzamos a hacer el estado explícito, reemplazamos funciones imperativas implícitas del tipo -> unit con funciones explícitas del tipo -> state -> .. -> state (aceptamos el estado como parámetro, devolvemos el estado nuevo).Por lo tanto, la exploración lista imperativo, que se veía así:

f edge_1 ; f edge_2 ; f edge_3 

Ahora se ve así:

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

que es exactamente lo que hace List.fold_left f state [edge_1 ; edge_2 ; edge_3]. Tocando mi propio cuerno, pero tengo a nice article about this here.

El tercer punto es que la operación "agregar elemento para establecer", al usar listas para representar conjuntos, se escribe simplemente como element :: set, porque esta es una operación que devuelve un nuevo conjunto (lista) que contiene todos los elementos de el conjunto original junto con el nuevo elemento. Esto deja el conjunto original intacto (lo cual es bueno por las razones descritas en el paso uno) mientras se usa una cantidad constante de memoria (crea una celda cons: un par simple cabeza-cola que contiene una referencia al elemento y una referencia al conjunto): no solo obtiene capacidades de deshacer, sino que lo hace sin costo adicional.

El algoritmo anterior "inserta" nodos en visited comenzando con las hojas de la exploración DFS, que en su caso representan los nodos que deberían hacerse al final. En resumen, la lista devuelta está clasificada topológicamente, pero podría no contener todos los elementos porque el punto de partida podría no ser el único elemento raíz (o incluso ser un elemento raíz). Entonces, aquí hay un paso de procesamiento adicional que consiste en tomar otro nodo del gráfico hasta que se haya explorado todo el gráfico.

O, en otras palabras, a partir de una exploración nueva DFS de cada nodo en el gráfico, pero ignorando cualquier nodos previamente explorado - lo que equivale a llevar la lista de elementos visitados de una exploración DFS a la siguiente .

Usando un pequeño pellizco en nuestro algoritmo anterior, esto tiene sólo dos líneas:

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 

El truco consiste en permitir que la persona que llama de dfs para especificar la lista de nodos ya visitados. Llevar a cabo la lista de nodos visitados al iniciar un DFS desde cada nodo se hace usando List.fold_left exactamente como antes.

EDIT: aparte del tipo de la excepción, no hay nada aquí que restrinja el tipo de los elementos en el gráfico. Sin embargo, una excepción no puede ser polimórfica, por lo que tiene dos posibles soluciones. La primera consiste en renunciar a la realidad de devolver cualquier datos junto con la excepción:

exception CycleFound 

... raise CycleFound ... 

Esto revertirá el tipo de toposort de nuevo a una más genérica ('a * ('a list)) list -> 'a list.

La otra solución es bastante avanzada OCaml: lo necesario para hacer el móduloque contiene la excepción y la clasificación topológica polimórficos en ese tipo específico, de la siguiente manera:

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 

Esto haría que el tipo de Topo(Node).sort ser (Node.t * (Node.t list)) list -> Node.t list, lo que significa que puede ordenar cualquier tipo que se desea mediante la definición de un módulo de nodo con ese tipo:

y
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 
+0

La primera pregunta que tengo, es sobre la excepción 'CycleFound'. No estoy seguro, cómo debería definirlo, parece ser una especie de excepción _parametrizada_. Quiero decir, puedo escribir 'excepción CycleFound de int list ;;', pero luego 'topsort' tiene signature:' val toposort: (int * ('a * int list)) list -> int list = 'y me pregunto , ¿qué debo escribir, tener '('a * (' a * 'a list)'? – justme

+0

Ah, parece que se había cometido un error ('let _, edges =' en lugar de 'let edges =') que hizo que los tipos se volvieran locos. La firma debería ser '(int * (int list)) list -> int list' now - y usted debería definir' exception CycleFound of int list' (el parámetro es el ciclo: una lista de nodos). –

+0

Y si quisiera ordenar por ejemplo strings e int usando la misma función, ¿qué debo hacer? Sé, puedo escribir un par de funciones, cada una para un tipo particular, una para cadena, una para int, etc. Supongo que no es una buena idea. Por lo tanto, estoy pensando en cómo editar Tu código, para tener una clasificación topológica general, no solo para el tipo int. – justme