2009-12-30 26 views
17

Ho un elenco di elementi che hanno un partial order relation, i. e, l'elenco può essere considerato un partially ordered set. Voglio ordinare questo elenco nello stesso modo in questo question. Come correttamente risposto lì, questo è noto come topological sorting.topologico Ordinamento utilizzando LINQ

C'è una ragionevolmente semplice algoritmo noto per risolvere il problema. Voglio un'implementazione simile a LINQ.

Ho già provato a utilizzare il metodo di estensione OrderBy, ma sono abbastanza sicuro che non è in grado di eseguire l'ordinamento topologico. Il problema è che l'interfaccia IComparer<TKey> non è in grado di rappresentare un ordine parziale. Questo accade perché il metodo Compare può restituire fondamentalmente 3 tipi di valori: nullo, negativo e positivo, che significa sono-uguali, è meno-che e è-più-quindi , rispettivamente. Una soluzione di lavoro sarebbe possibile solo se ci fosse un modo per restituire non correlato.

Dal mio punto di vista di parte, la risposta che sto cercando potrebbe essere composto da un'interfaccia IPartialOrderComparer<T> e un metodo di estensione in questo modo:

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

Come potrebbe questo essere attuata? Come sarà l'interfaccia IPartialOrderComparer<T>? Consiglieresti un approccio diverso? Sono ansioso di vederlo. Forse c'è un modo più bello per rappresentare l'ordine parziale, non lo so.

risposta

15

Suggerirei di utilizzare la stessa interfaccia IComparer, ma di scrivere il metodo di estensione in modo da interpretare 0 come non correlato.In un ordinamento parziale, se gli elementi A e B sono uguali il loro ordine non importa, come-saggio se sono estranei - devi solo ordinare loro con rispetto agli elementi con cui hanno definito i rapporti.

Ecco un esempio che fa un ordinamento parziale di interi pari e dispari:

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()); 
     } 
    } 
} 

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

+0

Vorrei ree, questo è il modo più ragionevole per rappresentare l'ordine parziale. Anche se avessimo un modo per vedere se gli elementi sono comparabili o meno, non sarebbe chiaro dove mettere qualcosa in relazione a qualcosa a cui non è correlato. L'uguaglianza sembra essere il modo più semplice per fare questo – luke

+0

Grazie per la risposta. Non ho ancora il tempo di esaminarlo profondamente. A prima vista, temo che l'uso di questi "default" potrebbe nascondere alcuni bug. Ad esempio, 'default (int)' è zero, ed è difficilmente il valore int minimo. Hai provato i valori negativi? Ad ogni modo, ci proverò domani mattina. – jpbochi

+0

Ok, il codice funziona nonostante i "default". Qualsiasi valore inizialmente messo sulle variabili 'minime' viene sovrascritto nel primo 'foreach'. A proposito, il primo 'foreach' può essere scartato abbastanza facilmente. Sto testando alcune possibili ottimizzazioni sul tuo codice. Funziona molto comunque. :) – jpbochi

2

Beh, non sono sicuro che questo modo di gestirlo sia il modo migliore, ma potrei sbagliarmi.

Il modo tipico di gestire l'ordinamento topologico consiste nell'utilizzare un grafico e per ogni iterazione rimuovere tutti i nodi che non dispongono di una connessione in entrata e rimuovere contemporaneamente tutte le connessioni in uscita da tali nodi. I nodi rimossi sono l'output dell'iterazione. Ripeti fino a quando non puoi rimuovere altri nodi.

Tuttavia, al fine di ottenere quei collegamenti, in primo luogo, con il metodo, si avrebbe bisogno:

  1. Metodo (il vostro operatore di confronto), che potrebbe dire "prima che", ma anche "nessuna informazione per questi due "
  2. Iterate tutte le combinazioni, creando connessioni per tutte le combinazioni per le quali il comparatore restituisce un ordine.

In altre parole, il metodo sarebbe probabilmente definito in questo modo:

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

e poi tornare null quando non ha una risposta definitiva per due chiavi.

Il problema a cui sto pensando è la parte "itera tutte le combinazioni". Forse c'è un modo migliore per gestirlo, ma non lo vedo.

1

credo che Lasse V. Karlsen's answer si trova sulla destra pista, ma non mi piace nascondiglio del metodo Compare (o un'interfaccia separata per quella materia, che non si estende da IComparable<T>).

Invece, preferirei vedere qualcosa di simile:

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

questo modo, si hanno ancora un'implementazione IComparer<T> che può essere utilizzato in altri luoghi che richiedono IComparer<T>.

Tuttavia, si richiede anche per indicare la relazione delle istanze di T tra loro con il valore di ritorno nel modo seguente (simile a IComparable<T>):

  • null - casi non sono comparabili a ciascun altro.
  • 0 - le istanze sono confrontabili tra loro.
  • 0 - x è una chiave comparabile, ma y non lo è.

  • < 0 - y è una chiave paragonabile, ma x non è.

Naturalmente, non sarà possibile ottenere ordinamento parziale quando passa un'implementazione di questo per tutto ciò che si aspetta un IComparable<T> (e va notato che la risposta di Lasse V. Karlsen non risolve questo o) come quello che si bisogno non può essere rappresentato in un semplice Confronto metodo che prende due istanze di T e restituisce un int.

Per completare la soluzione, è necessario fornire un metodo di ordinazione personalizzato (oltre a ThenBy, OrderByDescending e ThenByDescending) che accetta il nuovo parametro di istanza (come già indicato). L'implementazione sarebbe simile a questa:

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

interfaccia per definire rapporti ordine parziale:

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

Compare dovrebbe restituire -1 se x < y, 0 se x = y, 1 se y < x e null se x e y sono non comparabile.

Il nostro obiettivo è quello di restituire un ordinamento degli elementi nell'ordine parziale, che rispetta l'enumerazione. Cioè, cerchiamo una sequenza e_1, e_2, e_3, ..., e_n degli elementi nell'ordine parziale tale che se i <= j e e_i è paragonabile a e_j quindi e_i <= e_j. Farò questo utilizzando una ricerca in profondità.

classe che implementa ordinamento topologico utilizzando in profondità di ricerca:

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(); 
    } 
} 

classe Helper necessaria per la marcatura di nodi come visitato mentre si fa in profondità di ricerca:

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(); 
    } 
} 

faccio alcuna pretesa che questo è la migliore implementazione dell'algoritmo ma credo che sia una corretta implementazione. Inoltre, non ho restituito un IOrderedEnumerable come richiesto ma è facile farlo una volta arrivati ​​a questo punto.

L'algoritmo funziona facendo una ricerca in profondità attraverso gli elementi che aggiungono un elemento e per l'ordinamento lineare (rappresentato da _sorted nell'algoritmo) se abbiamo già aggiunto tutti i predecessori di e sono già stati aggiunti alla ordinazione . Quindi, per ogni elemento e, se non lo abbiamo già visitato, visitare i suoi predecessori e quindi aggiungere e. Così, questo è il nucleo dell'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); 
    } 
} 

Come esempio, si consideri la parziale ordinamento definite su sottoinsiemi di {1, 2, 3} dove X < Y se X è un sottoinsieme di Y. A implementare ciò come segue:

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; 
    } 
} 

Poi con sets definita come l'elenco di sottoinsiemi di {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()); 

Ciò provoca l'ordine:

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

che rispetta l'ordine parziale.

E 'stato molto divertente. Grazie.

+0

Grazie per la risposta. Sono felice che sia stato divertente per te. Ci proverò domani. Un dettaglio che ho notato è che dici che stai usando una ricerca in ampiezza, ma il tuo codice ha una classe 'DepthFirstSearch'. BTW, testare la soluzione con i set è stata molto accurata. – jpbochi

+0

Oops. Grazie per averlo capito. Ho usato una prima ricerca approfondita. – jason

+0

Questo è un buon approccio. Vi sono alcune ottimizzazioni/semplificazioni facili. Per cominciare, ho testato la soluzione utilizzando il normale 'IComparer' al posto di' IPartialComparer' e ha funzionato correttamente. Inoltre, la classe 'TopologicalSorter' potrebbe essere statica. Ad ogni modo, l'approccio seguito da ** tehMick ** sembra più semplice e veloce. Credo che dovrò accettare la sua risposta. – jpbochi

8

Questa è la versione ottimizzata e rinnovata di tehMick's answer.

Una modifica che ho apportato è stata la sostituzione della lista reale dei valori lasciati a produrre per un elenco logico. Per questo, ho array di due dimensioni della stessa dimensione. Uno ha tutti i valori, e gli altri contiene bandiere che indicano se ogni valore è stato restituito. In questo modo, evito il costo di dover ridimensionare un vero List<Key>.

Un altro cambiamento è che sto leggendo tutte le chiavi solo una volta all'inizio dell'iterazione. Per ragioni che non riesco a ricordare ora (forse e 'stato solo il mio istinto) non mi piace l'idea di chiamare la funzione keySelector diverse volte.

Gli ultimi ritocchi sono stati la convalida dei parametri e un sovraccarico supplementare che utilizza un comparatore di chiavi implicito. Spero che il codice sia abbastanza leggibile. Controlla.

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

grazie mille a tutti, a partire dalla risposta di Eric Mickelsen ho si avvicinò con la mia versione, come io preferisco usare i valori nulli per indicare alcuna relazione, come ha detto 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)); 
    } 

poi ho la seguente interfaccia per l'operatore di confronto

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

e questa classe helper

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); 
    } 
} 

che permette di abbellire l'uso un po 'così i miei test può è simile al seguente

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); 
    });