2009-12-30 10 views
17

У меня есть список предметов, имеющих partial order relation, i. e, список можно рассматривать как partially ordered set. Я хочу отсортировать этот список так же, как в этом question. Как правильно ответил там, это называется topological sorting.Топологическая сортировка с использованием LINQ

Существует достаточно простой известный алгоритм для решения проблемы. Я хочу использовать LINQ-подобную реализацию.

Я уже пытался использовать метод расширения OrderBy, но я уверен, что он не в состоянии сделать топологическую сортировку. Проблема в том, что интерфейс IComparer<TKey> не может представлять частичный порядок. Это происходит потому, что метод Compare может вернуться в основном 3 вида значений: нулевого, отрицательного и положительного, что означает является равными, является менее чем и это-больше-того , соответственно. Рабочее решение было бы возможно только в том случае, если был способ возврата не связаны.

С моей предвзятой точки зрения, ответ я ищу может быть сочиненные IPartialOrderComparer<T> интерфейс и метод расширения, как это:

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

Как это будет реализовано? Как выглядит интерфейс IPartialOrderComparer<T>? Вы порекомендовали бы другой подход? Я очень хочу это увидеть. Может быть, есть лучший способ представить частичный порядок, я не знаю.

ответ

15

Я бы предложил использовать тот же интерфейс IComparer, но написал метод расширения, чтобы интерпретировать 0 как не относящийся.В частичном упорядочении, если элементы a и b равны, их порядок не имеет значения, так же, если они не связаны друг с другом - вам нужно только заказать их в отношении элементов, с которыми они определили отношения.

Ниже приведен пример, который делает частичное упорядочение четных и нечетных чисел:

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

Результат: 4, 8, 3, 5, 7, 10

+0

Я бы ag ree, это самый разумный способ представить частичный порядок. Даже если бы у нас был такой способ, чтобы увидеть, сопоставимы ли элементы или нет, было бы неясно, где поставить что-то по отношению к чему-то, к чему это не связано. Равенство, кажется, самый простой способ сделать это – luke

+0

Спасибо за ответ. У меня нет времени изучать его глубоко. На первый взгляд, я боюсь, что использование этих 'default' там может скрыть некоторые ошибки. Например, 'default (int)' равно нулю, и это вряд ли минимальное значение int. Вы пробовали отрицательные значения? Во всяком случае, я попробую завтра утром. – jpbochi

+0

Хорошо, код работает, несмотря на 'default'. Любое значение, первоначально заданное на «минимальных» переменных, перезаписывается в первом 'foreach'. BTW, первый «foreach» можно легко отбросить. Я тестирую некоторые возможные варианты оптимизации вашего кода. Работает в любом случае. :) – jpbochi

2

Ну, я не уверен, что этот способ обращения с ним - лучший способ, но я могу ошибаться.

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

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

  1. Метод (ваш компаратором), который мог бы сказать «это до этого», но и «нет информации для этих двух "
  2. Итерируйте все комбинации, создавая соединения для всех комбинаций, для которых компаратор возвращает заказ.

Другими словами, метод, вероятно, определяется следующим образом:

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

, а затем вернуться null, когда он не имеет окончательного ответа на два ключа.

Проблема, о которой я думаю, это часть «итерации всех комбинаций». Возможно, есть лучший способ справиться с этим, но я этого не вижу.

1

Я считаю, что Lasse V. Karlsen's answer находится на правой трек, но мне не нравится скрывать метод Compare (или отдельный интерфейс в этом отношении, который не распространяется от IComparable<T>).

Вместо этого, я предпочел бы увидеть что-то вроде этого:

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

Таким образом, вы все еще есть реализацию IComparer<T>, которые могут быть использованы в других местах, требующих IComparer<T>.

Однако он также требует, чтобы указать взаимосвязь экземпляров Т друг к другу с возвращаемым значением следующим образом (по аналогии с IComparable<T>):

  • нуля - экземпляры не сопоставимы друг Другие.
  • 0 - экземпляры сопоставимы друг с другом.
  • 0 - х сопоставимый ключ, но у нет.

  • < 0 - y сопоставимый ключ, но x нет.

Конечно, вы не получите частичный порядок при переходе к реализации этого к чему-либо, что ожидает IComparable<T> (и это следует отметить, что ответ Ласс В. Карлсен не решает это либо), как то, что вы потребность не может быть представлена ​​в простом методе сравнения, который принимает два экземпляра T и возвращает int.

Чтобы закончить решение, вы должны предоставить собственный метод OrderBy (а также ThenBy, OrderByDescending и ThenByDescending), который примет новый параметр экземпляра (как вы уже указали). Реализация будет выглядеть примерно так:

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

Интерфейс для определения отношения частичного порядка:

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

Compare должен возвращать -1 если x < y, 0 если x = y, 1 если y < x и null если x и y являются не сопоставимы.

Наша цель - вернуть порядок элементов в частичном порядке, который соответствует перечислению. То есть, мы ищем последовательность элементов в частичном порядке, так что если i <= j и e_i сопоставимо с e_j, то e_i <= e_j. Я сделаю это, используя поиск по глубине.

Класс, который реализует топологическую сортировку с помощью поиска в глубину:

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

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

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

Я не утверждаю, что это является наилучшей реализацией алгоритма, но я считаю, что это правильная реализация. Кроме того, я не возвращал IOrderedEnumerable, как вы просили, но это легко сделать, когда мы находимся на этом этапе.

Алгоритм работы, выполнив поиска в глубину через элементы добавления элемента e в линейном порядке (в лице _sorted в алгоритме), если мы уже добавили все предшественники e уже были добавлены к порядку , Следовательно, для каждого элемента e, если мы его еще не посетили, посетите его предшественники, а затем добавьте e. Таким образом, это является ядром алгоритма:

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

В качестве примера, рассмотрим частичную упорядоченность, определенную на подмножествах {1, 2, 3} где X < Y, если X является подмножеством Y. Я реализовать это следующим образом:

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

Затем с sets определяется как список подмножеств из {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()); 

Это приводит к упорядочению:

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

, уважающее частичный порядок.

Это было очень весело. Благодарю.

+0

Спасибо за ответ. Я рад, что это было весело для вас. Я попробую это через завтра. Одна деталь, которую я заметил, - это то, что вы говорите, что собираетесь использовать поиск по ширине, но ваш код имеет класс «DepthFirstSearch». BTW, тестирование решения с наборами было очень аккуратным. – jpbochi

+0

Упс. Спасибо, что поймал это. Я использовал первый поиск глубины. – jason

+0

Это хороший подход. Есть несколько возможных простых оптимизаций/упрощений. Для начала я тестировал ваше решение, используя обычный 'IComparer' вместо вашего' IPartialComparer', и он работал правильно. Кроме того, класс «TopologicalSorter» может быть статичным. Во всяком случае, подход, который следовал за ** tehMick **, кажется более простым и быстрым. Наверное, мне придется принять его ответ. – jpbochi

8

Это моя оптимизированная и обновленная версия tehMick's answer.

Одно изменение, которое я сделал, заменяло реальный список значений, оставшихся для получения логического списка. Для этого у меня два массива размера одинакового размера. У одного есть все значения, а остальные содержат флажки, указывающие, было ли получено каждое значение. Таким образом, я избегаю затрат на изменение размера реального List<Key>.

Еще одно изменение заключается в том, что я читаю все ключи только один раз в начале итерации. По причинам, которые я не могу вспомнить сейчас (возможно, это был только мой инстинкт) Мне не нравится идея вызвать функцию keySelector несколько раз.

Последними касаниями была проверка параметров и дополнительная перегрузка, которая использует неявный ключ-сопоставитель. Надеюсь, код достаточно читаем. Проверьте это.

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

большое спасибо всем, начиная от ответа Эрика Mickelsen, я уже придумал свою версию, как я предпочитаю использовать нулевые значения, чтобы указать, никакого отношения, как сказал Лассе В. Карлсен.

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

то у меня есть следующий интерфейс для компаратора

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

и этого вспомогательного класса

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

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

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); 
    }); 
Смежные вопросы