2009-12-30 26 views
17

Tengo una lista de artículos que tienen un partial order relation, i. e, la lista se puede considerar partially ordered set. Quiero ordenar esta lista de la misma manera que en este question. Como se contestó correctamente allí, esto se conoce como topological sorting.Clasificación topológica usando LINQ

Hay un algoritmo conocido razonablemente simple para resolver el problema. Quiero una implementación similar a LINQ.

Ya he intentado utilizar el método de extensión OrderBy, pero estoy bastante seguro de que no es capaz de realizar una clasificación topológica. El problema es que la interfaz IComparer<TKey> no puede representar un orden parcial. Esto sucede porque el método Compare puede volver básicamente 3 tipos de valores: cero, negativo, y positivo, lo que significa son de igual, es-menos-que, y es-más-a continuación , respectivamente. Una solución de trabajo solo sería posible si existiera una forma de devolver sin relación.

Desde mi punto de vista sesgado, la respuesta Busco podría estar compuesto por una interfaz IPartialOrderComparer<T> y un método de extensión de esta manera:

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    IPartialOrderComparer<TKey> comparer 
); 

¿Cómo sería esto se implementará? ¿Cómo se vería la interfaz IPartialOrderComparer<T>? ¿Recomendarías un enfoque diferente? Estoy ansioso por verlo. Tal vez haya una manera más agradable de representar el orden parcial, no lo sé.

Respuesta

15

Sugeriría utilizar la misma interfaz IComparer, pero escribir el método de extensión para interpretar 0 como no relacionado.En un ordenamiento parcial, si los elementos a y b son iguales, su orden no es importante, del mismo modo si no están relacionados, solo debe ordenarlos con respecto a los elementos con los que tienen relaciones definidas.

Aquí hay un ejemplo que hace una ordenación parcial de pares e enteros impares:

namespace PartialOrdering 
{ 
    public static class Enumerable 
    { 
     public static IEnumerable<TSource> PartialOrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer) 
     { 
      List<TSource> list = new List<TSource>(source); 
      while (list.Count > 0) 
      { 
       TSource minimum = default(TSource); 
       TKey minimumKey = default(TKey); 
       foreach (TSource s in list) 
       { 
        TKey k = keySelector(s); 
        minimum = s; 
        minimumKey = k; 
        break; 
       } 
       foreach (TSource s in list) 
       { 
        TKey k = keySelector(s); 
        if (comparer.Compare(k, minimumKey) < 0) 
        { 
         minimum = s; 
         minimumKey = k; 
        } 
       } 
       yield return minimum; 
       list.Remove(minimum); 
      } 
      yield break; 
     } 

    } 
    public class EvenOddPartialOrdering : IComparer<int> 
    { 
     public int Compare(int a, int b) 
     { 
      if (a % 2 != b % 2) 
       return 0; 
      else if (a < b) 
       return -1; 
      else if (a > b) 
       return 1; 
      else return 0; //equal 
     } 
    } 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      IEnumerable<Int32> integers = new List<int> { 8, 4, 5, 7, 10, 3 }; 
      integers = integers.PartialOrderBy<Int32, Int32>(new Func<Int32, Int32>(delegate(int i) { return i; }), new EvenOddPartialOrdering()); 
     } 
    } 
} 

Resultado: 4, 8, 3, 5, 7, 10

+0

I would ag ree, esta es la forma más razonable de representar el orden parcial. Incluso si tuviéramos una forma de ver si los elementos son comparables o no, no estaría claro dónde poner algo en relación con algo que no está relacionado. La igualdad parece ser la forma más directa de hacerlo – luke

+0

Gracias por la respuesta. No tengo tiempo para examinarlo profundamente todavía. A primera vista, me temo que el uso de esos 'predeterminados' puede ocultar algunos errores. Por ejemplo, 'default (int)' es cero, y apenas es el valor int mínimo. ¿Has probado los valores negativos? De todos modos, lo intentaré mañana por la mañana. – jpbochi

+0

Ok, el código funciona a pesar de los 'predeterminados'. Cualquier valor puesto inicialmente en las variables 'mínimas' se sobrescribe en el primer 'foreach'. Por cierto, el primer "foreach" se puede descartar con bastante facilidad. Estoy probando algunas posibles optimizaciones en tu código. Funciona de todos modos de todos modos. :) – jpbochi

2

Bueno, no estoy seguro de que esta forma de manejarlo sea la mejor, pero podría estar equivocado.

La forma típica de manejar la clasificación topológica es mediante el uso de un gráfico, y para cada iteración, elimine todos los nodos que no tengan una conexión de entrada, y elimine simultáneamente todas las conexiones salientes de esos nodos. Los nodos eliminados son el resultado de la iteración. Repita hasta que no pueda eliminar más nodos.

Sin embargo, con el fin de conseguir esas conexiones en el primer lugar, con su método, lo que se necesita:

  1. Un método (el comparador) que podría decir "esto antes de que", sino también "no hay información para estos dos "
  2. Iterate todas las combinaciones, creando conexiones para todas las combinaciones para las cuales el comparador devuelve un pedido.

En otras palabras, el método probablemente se define así:

public Int32? Compare(TKey a, TKey b) { ... } 

y luego regresar null cuando no tiene una respuesta concluyente por dos llaves.

El problema en el que estoy pensando es en la parte "iterar todas las combinaciones". Tal vez haya una mejor manera de manejar esto, pero no lo estoy viendo.

1

creo que Lasse V. Karlsen's answer está a la derecha rastrear, pero no me gusta esconder el método Comparar (o una interfaz separada para ese asunto que no se extiende desde IComparable<T>).

En cambio, yo prefiero ver algo como esto:

public interface IPartialOrderComparer<T> : IComparer<T> 
{ 
    int? InstancesAreComparable(T x, T y); 
} 

De esta manera, usted todavía tiene una implementación de IComparer<T> que puede ser utilizado en otros lugares que requieren IComparer<T>.

Sin embargo, también se requiere que indica la relación de los casos de T entre sí con el valor de retorno de la siguiente manera (similar a IComparable<T>):

  • null - casos no son comparables a cada otro.
  • 0 - las instancias son comparables entre sí.
  • 0 - x es una clave comparable, pero y no lo es.

  • < 0 - y es una clave comparable, pero x no lo es.

Por supuesto, usted no conseguirá ordenación parcial cuando pasa una implementación de este a cualquier cosa que se espera un IComparable<T> (y cabe señalar que la respuesta de Lasse V. Karlsen no resuelve este tampoco) como lo que se la necesidad no se puede representar en un método simple de comparación que toma dos instancias de T y devuelve un int.

Para finalizar la solución, debe proporcionar un método de extensión personalizado OrderBy (así como ThenBy, OrderByDescending y ThenByDescending también) que aceptará el nuevo parámetro de instancia (como ya ha indicado). La implementación sería algo como esto:

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(  
    this IEnumerable<TSource> source,  
    Func<TSource, TKey> keySelector,  
    IPartialOrderComparer<TKey> comparer) 
{ 
    return Enumerable.OrderBy(source, keySelector, 
     new PartialOrderComparer(comparer); 
} 

internal class PartialOrderComparer<T> : IComparer<T> 
{ 
    internal PartialOrderComparer(IPartialOrderComparer<T> 
     partialOrderComparer) 
    { 
     this.partialOrderComparer = partialOrderComparer; 
    } 

    private readonly IPartialOrderComparer<T> partialOrderComparer; 

    public int Compare(T x, T y) 
    { 
     // See if the items are comparable. 
     int? comparable = partialOrderComparable. 
      InstancesAreComparable(x, y); 

     // If they are not comparable (null), then return 
     // 0, they are equal and it doesn't matter 
     // what order they are returned in. 
     // Remember that this only to determine the 
     // values in relation to each other, so it's 
     // ok to say they are equal. 
     if (comparable == null) return 0; 

     // If the value is 0, they are comparable, return 
     // the result of that. 
     if (comparable.Value == 0) return partialOrderComparer.Compare(x, y); 

     // One or the other is uncomparable. 
     // Return the negative of the value. 
     // If comparable is negative, then y is comparable 
     // and x is not. Therefore, x should be greater than y (think 
     // of it in terms of coming later in the list after 
     // the ordered elements). 
     return -comparable.Value;    
    } 
} 
1

interfaz para definir la relación de orden parcial:

interface IPartialComparer<T> { 
    int? Compare(T x, T y); 
} 

Compare deberían volver -1 si x < y, 0 si x = y, 1 si y < x y null si x y y son No es comparable.

Nuestro objetivo es devolver un orden de los elementos en el orden parcial que respeta la enumeración. Es decir, buscamos una secuencia e_1, e_2, e_3, ..., e_n de los elementos en el orden parcial tal que si i <= j y e_i es comparable a e_j entonces e_i <= e_j. Haré esto usando una búsqueda en profundidad.

clase que implementa clasificación topológica utilizando primero en profundidad de búsqueda:

class TopologicalSorter { 
    class DepthFirstSearch<TElement, TKey> { 
     readonly IEnumerable<TElement> _elements; 
     readonly Func<TElement, TKey> _selector; 
     readonly IPartialComparer<TKey> _comparer; 
     HashSet<TElement> _visited; 
     Dictionary<TElement, TKey> _keys; 
     List<TElement> _sorted; 

     public DepthFirstSearch(
      IEnumerable<TElement> elements, 
      Func<TElement, TKey> selector, 
      IPartialComparer<TKey> comparer 
     ) { 
      _elements = elements; 
      _selector = selector; 
      _comparer = comparer; 
      var referenceComparer = new ReferenceEqualityComparer<TElement>(); 
      _visited = new HashSet<TElement>(referenceComparer); 
      _keys = elements.ToDictionary(
       e => e, 
       e => _selector(e), 
       referenceComparer 
      ); 
      _sorted = new List<TElement>(); 
     } 

     public IEnumerable<TElement> VisitAll() { 
      foreach (var element in _elements) { 
       Visit(element); 
      } 
      return _sorted; 
     } 

     void Visit(TElement element) { 
      if (!_visited.Contains(element)) { 
       _visited.Add(element); 
       var predecessors = _elements.Where(
        e => _comparer.Compare(_keys[e], _keys[element]) < 0 
       ); 
       foreach (var e in predecessors) { 
        Visit(e); 
       } 
       _sorted.Add(element); 
      } 
     } 
    } 

    public IEnumerable<TElement> ToplogicalSort<TElement, TKey>(
     IEnumerable<TElement> elements, 
     Func<TElement, TKey> selector, IPartialComparer<TKey> comparer 
    ) { 
     var search = new DepthFirstSearch<TElement, TKey>(
      elements, 
      selector, 
      comparer 
     ); 
     return search.VisitAll(); 
    } 
} 

Clase auxiliar necesaria para el marcado de los nodos como visitado mientras se hace primero en profundidad de búsqueda:

class ReferenceEqualityComparer<T> : IEqualityComparer<T> { 
    public bool Equals(T x, T y) { 
     return Object.ReferenceEquals(x, y); 
    } 

    public int GetHashCode(T obj) { 
     return obj.GetHashCode(); 
    } 
} 

No pretendo que este es la mejor implementación del algoritmo, pero creo que es una implementación correcta. Además, no me vuelvo un IOrderedEnumerable como usted pidió pero que es fácil de hacer una vez que estamos en este punto.

El algoritmo funciona haciendo una búsqueda en profundidad a través de los elementos de la adición de un elemento e a la ordenación lineal (representado por _sorted en el algoritmo) si ya hemos añadido todos los predecesores de e ya se han agregado a la ordenación . Por lo tanto, para cada elemento e, si todavía no hemos visitado, visitar sus predecesores y luego añadir e. Por lo tanto, este es el núcleo del algoritmo:

public void Visit(TElement element) { 
    // if we haven't already visited the element 
    if (!_visited.Contains(element)) { 
     // mark it as visited 
     _visited.Add(element); 
     var predecessors = _elements.Where(
      e => _comparer.Compare(_keys[e], _keys[element]) < 0 
     ); 
     // visit its predecessors 
     foreach (var e in predecessors) { 
      Visit(e); 
     } 
     // add it to the ordering 
     // at this point we are certain that 
     // its predecessors are already in the ordering 
     _sorted.Add(element); 
    } 
} 

Como ejemplo, considerar la parcial-pedido definido en subconjuntos de {1, 2, 3} donde X < Y si X es un subconjunto de Y. Implemento esto como sigue:

public class SetComparer : IPartialComparer<HashSet<int>> { 
    public int? Compare(HashSet<int> x, HashSet<int> y) { 
     bool xSubsety = x.All(i => y.Contains(i)); 
     bool ySubsetx = y.All(i => x.Contains(i)); 
     if (xSubsety) { 
      if (ySubsetx) { 
       return 0; 
      } 
      return -1; 
     } 
     if (ySubsetx) { 
      return 1; 
     } 
     return null; 
    } 
} 

Luego, con sets definida como la lista de subconjuntos de {1, 2, 3}

List<HashSet<int>> sets = new List<HashSet<int>>() { 
    new HashSet<int>(new List<int>() {}), 
    new HashSet<int>(new List<int>() { 1, 2, 3 }), 
    new HashSet<int>(new List<int>() { 2 }), 
    new HashSet<int>(new List<int>() { 2, 3}), 
    new HashSet<int>(new List<int>() { 3 }), 
    new HashSet<int>(new List<int>() { 1, 3 }), 
    new HashSet<int>(new List<int>() { 1, 2 }), 
    new HashSet<int>(new List<int>() { 1 }) 
}; 
TopologicalSorter s = new TopologicalSorter(); 
var sorted = s.ToplogicalSort(sets, set => set, new SetComparer()); 

Esto resulta en la Orden:

{}, {2}, {3}, {2, 3}, {1}, {1, 3}, {1, 2}, {1, 2, 3} 

que respeta el orden parcial.

Eso fue muy divertido. Gracias.

+0

Gracias por la respuesta. Me alegra que fue divertido para ti. Lo intentaré mañana. Un detalle que noté es que dices que vas a usar una búsqueda amplia, pero tu código tiene una clase 'DepthFirstSearch'. Por cierto, probar la solución con sets fue muy limpio. – jpbochi

+0

Vaya. Gracias por atrapar eso. Usé una primera búsqueda en profundidad. – jason

+0

Este es un buen enfoque. Hay algunas posibles optimizaciones/simplificaciones fáciles. Para empezar, probé su solución usando el 'IComparer' común en lugar de su' IPartialComparer' y funcionó correctamente. Además, la clase 'TopologicalSorter' podría ser estática. De todos modos, el enfoque que siguió ** tehMick ** parece más simple y rápido. Creo que tendré que aceptar su respuesta. – jpbochi

8

Esta es mi versión optimizada y reformado de tehMick's answer.

Un cambio que hice reemplazaba la lista real de los valores restantes para una lista lógica. Para esto, tengo dos matrices de tamaño del mismo tamaño. Uno tiene todos los valores, y el otro contiene banderas que indican si se ha entregado cada valor. De esta manera, evitar que el coste de tener que cambiar el tamaño de un verdadero List<Key>.

Otro cambio es que estoy leyendo todas las claves de una sola vez al comienzo de la iteración. Por razones que no puedo recordar ahora (tal vez es sólo mi instinto) No me gusta la idea de llamar a las funciones keySelector varias veces.

Los últimos toques fue la validación de parámetros, y una sobrecarga adicional que utiliza un comparador de claves implícita. Espero que el código sea lo suficientemente legible. Echale un vistazo.

public static IEnumerable<TSource> PartialOrderBy<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector) 
{ 
    return PartialOrderBy(source, keySelector, null); 
} 

public static IEnumerable<TSource> PartialOrderBy<TSource, TKey>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    IComparer<TKey> comparer) 
{ 
    if (source == null) throw new ArgumentNullException("source"); 
    if (keySelector == null) throw new ArgumentNullException("keySelector"); 
    if (comparer == null) comparer = (IComparer<TKey>)Comparer<TKey>.Default; 

    return PartialOrderByIterator(source, keySelector, comparer); 
} 

private static IEnumerable<TSource> PartialOrderByIterator<TSource, TKey>(
    IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    IComparer<TKey> comparer) 
{ 
    var values = source.ToArray(); 
    var keys = values.Select(keySelector).ToArray(); 
    int count = values.Length; 
    var notYieldedIndexes = System.Linq.Enumerable.Range(0, count).ToArray(); 
    int valuesToGo = count; 

    while (valuesToGo > 0) 
    { 
     //Start with first value not yielded yet 
     int minIndex = notYieldedIndexes.First(i => i >= 0); 

     //Find minimum value amongst the values not yielded yet 
     for (int i=0; i<count; i++) 
     if (notYieldedIndexes[i] >= 0) 
     if (comparer.Compare(keys[i], keys[minIndex]) < 0) { 
      minIndex = i; 
     } 

     //Yield minimum value and mark it as yielded 
     yield return values[minIndex]; 
     notYieldedIndexes[minIndex] = -1; 
     valuesToGo--; 
    } 
} 
0

muchas gracias a todo el mundo, a partir de la respuesta de Eric Mickelsen he vinieron con mi versión como prefiero utilizar valores nulos para indicar ninguna relación como dijo Lasse V. Karlsen.

public static IEnumerable<TSource> PartialOrderBy<TSource>(
     this IEnumerable<TSource> source,    
     IPartialEqualityComparer<TSource> comparer) 
    { 
     if (source == null) throw new ArgumentNullException("source"); 
     if (comparer == null) throw new ArgumentNullException("comparer"); 


     var set = new HashSet<TSource>(source); 
     while (!set.IsEmpty()) 
     { 
      TSource minimum = set.First();     

      foreach (TSource s in set) 
      {      
       var comparison = comparer.Compare(s, minimum); 
       if (!comparison.HasValue) continue; 
       if (comparison.Value <= 0) 
       { 
        minimum = s;       
       } 
      } 
      yield return minimum; 
      set.Remove(minimum); 
     } 
    } 

public static IEnumerable<TSource> PartialOrderBy<TSource>(
     this IEnumerable<TSource> source, 
     Func<TSource, TSource, int?> comparer) 
    { 
     return PartialOrderBy(source, new PartialEqualityComparer<TSource>(comparer)); 
    } 

luego tengo la siguiente interfaz para el comparador

public interface IPartialEqualityComparer<T> 
{ 
    int? Compare(T x, T y); 
} 

y esta clase de ayuda

internal class PartialEqualityComparer<TSource> : IPartialEqualityComparer<TSource> 
{ 
    private Func<TSource, TSource, int?> comparer; 

    public PartialEqualityComparer(Func<TSource, TSource, int?> comparer) 
    { 
     this.comparer = comparer; 
    } 

    public int? Compare(TSource x, TSource y) 
    { 
     return comparer(x,y); 
    } 
} 

que permite embellecer el uso de un poco para mis ensayos podrá tiene el siguiente

var data = new int[] { 8,7,6,5,4,3,2,1,0 }; 
var partiallyOrdered = data.PartialOrderBy((x, y) => 
    { 
     if (x % 2 == 0 && y % 2 != 0) return null; 
     return x.CompareTo(y); 
    }); 
Cuestiones relacionadas