查找集合中的循环项(非传递项)

时间:2015-06-25 23:33:38

标签: c# .net linq recursion collections

问题:

我有一个简单的List<T>,我试图对它进行排序。但是,列表中的项目在可比性方面并不是全部可传递的,例如,对于例如我的List<T>看起来像是:

A
B
C
D
E

其中 A&gt; B B&gt; C C&gt; A 的。也可以像 A&gt;那样具有圆形的伟大。 B B&gt; C C&gt; D D&gt; ,即它不必总是一组3 。我想要的是在给定的List<T> 中找到所有圆形伟大的群体。例如,假设 A&gt; B> C> A A&gt; B> C> D> 是上面两个圆形组,我的输出应该看起来像:

List<List<T>> circulars = [[A, B, C, A], [A, B, C, D, A]]

List<List<T>> circulars = [[A, B, C], [A, B, C, D]]
// but in this case I do not want duplicates in the output. 
// For e.g., the output shouldn't have both [B, C, A] and [A, B, C]
// since both the groups refer to the same set of circular items A, B & C
// as B > C > A > B is also true. 
// But [B, A, C] is a different group (though nothing circular about it)

任何一个对我都没问题。我更喜欢小型(linquish)解决方案,但这看起来并不像最初看起来那么容易。可能是我错过了一些非常简单的事情。

情景:

这是体育分析的一部分,其中一个球员/球队将比另一个更强,反过来将比另一个更强,但最后一个将比第一个更强。我无法透露更多信息,但让我举一个体育运动的例子,特别是在网球和国际象棋中,个人比赛导致了这种情况。例如,就头对头而言,克拉姆尼克带领卡斯帕罗夫和卡斯帕罗夫领导卡尔波夫,但卡尔波夫领导克拉姆尼克。或者另一个例如,费德勒带领达维登科,达维登科领先纳达尔,但纳达尔领先费德勒。

我的班级看起来像这样:

class Player : IComparable<Player>
{
    // logic
}

这就是我的尝试:

  1. 首先生成所有可能的集合项排列,最小组大小为3.如[ABC],[A,C,B] ....,[A,B,C,D],[ A,B,D,C] ....等(这很慢)

  2. 然后浏览整个子组并检查模式。就像是否存在A&gt;的情况一样B> C> D(这个速度相当慢,但我很好)

  3. 最后浏览整个子组以删除重复的组,如[A,B,C]和[B,C,A]等。

  4. 代码:

    var players = [.....]; //all the players in the collection
    
    // first generate all the permutations possible in the list from size 3 
    // to players.Count
    var circulars = Enumerable.Range(3, players.Count - 3 + 1)
                   .Select(x => players.Permutations(x))
                   .SelectMany(x => x)
                   .Select(x => x.ToList())
    
    // then check in the each sublists if a pattern like A > B > C > A is 
    // generated                                                                          vv    this is the player comparison
                   .Where(l => l.Zip(l.Skip(1), (p1, p2) => new { p1, p2 }).All(x => x.p1 > x.p2) && l.First() < l.Last())
    
    // then remove the duplicate lists using special comparer
                   .Distinct(new CircularComparer<Player>())
                   .ToList();
    
    public static IEnumerable<IEnumerable<T>> Permutations<T>(this IEnumerable<T> list, int length)
    {
        if (length == 1) 
            return list.Select(t => new[] { t });
    
        return Permutations(list, length - 1)  
              .SelectMany(t => list.Where(e => !t.Contains(e)), (t1, t2) => t1.Concat(new[] { t2 }));
    }
    
    class CircularComparer<T> : IEqualityComparer<ICollection<T>>
    {
        public bool Equals(ICollection<T> x, ICollection<T> y)
        {
            if (x.Count != y.Count)
                return false;
    
            return Enumerable.Range(1, x.Count)
                  .Any(i => x.SequenceEqual(y.Skip(i).Concat(y.Take(i))));
        }
    
        public int GetHashCode(ICollection<T> obj)
        {
            return 0;
        }
    }
    

    这种方法的问题在于它非常慢。对于大约10个项目的集合,必须产生的排列是巨大的(接近100万个项目)。有没有更合理有效的方法?我不是追求最快的代码。这里有更好的递归方法吗?闻起来像。

3 个答案:

答案 0 :(得分:2)

情景......

  

[A,B,C,D,E]

     

其中A&gt; B,B> C,C> D,C> A,D&gt;甲

...可以使用A -> B表示A > B的惯例表示为有向图:

Graph

所以问题基本上是“如何在有向图中找到周期?”

要解决此问题,您可以使用Shared Web Credentials。我建议您查找此算法的一个很好的实现并将其应用到您的场景中。

答案 1 :(得分:1)

有许多用于枚举N个对象的排列的方法,使得可以从枚举中的索引有效地获得每个排列。一个如my tutorial on CUDOFY using the Travelling Salesman problem的摘录:

    /// <summary>Amended algorithm after SpaceRat (see Remarks): 
    /// Don't <b>Divide</b> when you can <b>Multiply</b>!</summary>
    /// <seealso cref="http://www.daniweb.com/software-development/cpp/code/274075/all-permutations-non-recursive"/> 
    /// <remarks>Final loop iteration unneeded, as element [0] only swaps with itself.</remarks>
  [Cudafy]
  public static float PathFromRoutePermutation(GThread thread, 
            long  permutation, int[,] path) {
     for (int city = 0; city < _cities; city++) { path[city, thread.threadIdx.x] = city; }

     var divisor = 1L;
     for (int city = _cities; city > 1L; /* decrement in loop body */) {
        var dest    = (int)((permutation / divisor) % city);
        divisor     *= city;

        city--;

        var swap                        = path[dest, thread.threadIdx.x];
        path[dest, thread.threadIdx.x]  = path[city, thread.threadIdx.x];
        path[city, thread.threadIdx.x]  = swap;
     }
     return 0;
    }
    #endregion
}

从这一点开始,人们可以很容易地并行地用圆形伟大来识别排列。首先可以使用CPU上的多个内核来提高性能,然后再使用GPU上可用的内核。经过反复调整旅行商问题  通过这种方式,我使用我的GPU将performance for the 11 cities case从超过14秒(仅使用CPU)提高到约0.25秒;改善50倍。

当然,您的里程将根据问题的其他方面以及您的硬件而有所不同。

答案 2 :(得分:0)

我可以依靠递归来大幅提升性能。我不是事先生成可能的序列的整个排列,而是现在通过集合来查找循环。为了帮助我,我创建了自己的循环引用(更大和更小的项目),以便我可以遍历。代码有点长。

这是基本的想法:

  1. 我创建了一个基本接口ICyclic<T>,它必须由Player类实现。

  2. 我遍历集合并分配较小和较大的项目(在Prepare方法中)。

  3. 我忽略了真正的错误(即集合中没有较少的项目)和非常好的(即集合中没有更多的项目)以避免无限递归并通常提高性能。绝对最好的和最差的不会对周期做出贡献。全部用Prepare方法完成。

  4. 现在每件商品都有一个小于商品的商品集合。并且集合中的项目将拥有自己的更糟糕项目的集合。等等。这是我递归遍历的路径。

  5. 在每一点上,将最后一项与访问路径中的第一项进行比较,以检测周期。

  6. 将周期添加到HashSet<T>以避免重复。定义了相等比较器以检测等效的循环列表。

  7. 代码:

    public interface ICyclic<T> : IComparable<T>
    {
        ISet<T> Worse { get; set; }
        ISet<T> Better { get; set; }
    }
    
    public static ISet<IList<T>> Cycles<T>(this ISet<T> input) where T : ICyclic<T>
    {
        input = input.ToHashSet();
        Prepare(input);
    
        var output = new HashSet<IList<T>>(new CircleEqualityComparer<T>());
        foreach (var item in input)
        {
            bool detected;
            Visit(item, new List<T> { item }, item.Worse, output, out detected);
        }
    
        return output;
    }
    
    static void Prepare<T>(ISet<T> input) where T : ICyclic<T>
    {
        foreach (var item in input)
        {
            item.Worse = input.Where(t => t.CompareTo(item) < 0).ToHashSet();
            item.Better = input.Where(t => t.CompareTo(item) > 0).ToHashSet();
        }
    
        Action<Func<T, ISet<T>>> exceptionsRemover = x =>
        {
            var exceptions = new HashSet<T>();
            foreach (var item in input.OrderBy(t => x(t).Count))
            {
                x(item).ExceptWith(exceptions);
                if (!x(item).Any())
                    exceptions.Add(item);
            }
    
            input.ExceptWith(exceptions);
        };
        exceptionsRemover(t => t.Worse);
        exceptionsRemover(t => t.Better);
    }
    
    static void Visit<T>(T item, List<T> visited, ISet<T> worse, ISet<IList<T>> output, 
                         out bool detected) where T : ICyclic<T>
    {
        detected = false;
    
        foreach (var bad in worse)
        {
            Func<T, T, bool> comparer = (t1, t2) => t1.CompareTo(t2) > 0;
    
            if (comparer(visited.Last(), visited.First()))
            {
                detected = true;
                var cycle = visited.ToList();
                output.Add(cycle);
            }
    
            if (visited.Contains(bad))
            {
                var cycle = visited.SkipWhile(x => !x.Equals(bad)).ToList();
                if (cycle.Count >= 3)
                {
                    detected = true;
                    output.Add(cycle);
                }
                continue;
            }
    
            if (bad.Equals(item) || comparer(bad, visited.Last()))
                continue;
    
            visited.Add(bad);
    
            Visit(item, visited, bad.Worse, output, out detected);
            if (detected)
                visited.Remove(bad);
        }
    }
    
    public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
    {
        return new HashSet<T>(source);
    }
    
    public class CircleEqualityComparer<T> : IEqualityComparer<ICollection<T>>
    {
        public bool Equals(ICollection<T> x, ICollection<T> y)
        {
            if (x.Count != y.Count)
                return false;
    
            return Enumerable.Range(1, x.Count)
                  .Any(i => x.SequenceEqual(y.Skip(i).Concat(y.Take(i))));
        }
    
        public int GetHashCode(ICollection<T> obj)
        {
            return unchecked(obj.Aggregate(0, (x, y) => x + y.GetHashCode()));
        }
    }
    

    原始回答 (来自OP)

    在正面,这更简洁。此外,由于它不依赖于递归,因此不需要ICyclic<T>约束,任何IComparable<T>都应该有效。 在负面,它在1月的糖蜜中很慢。

    public static IEnumerable<ICollection<T>> Cycles<T>(this ISet<T> input) where T : IComparable<T>
    {
        if (input.Count < 3)
            return Enumerable.Empty<ICollection<T>>();
    
        Func<T, T, bool> comparer = (t1, t2) => t1.CompareTo(t2) > 0;
    
        return Enumerable.Range(3, input.Count - 3 + 1)
              .Select(x => input.Permutations(x))
              .SelectMany(x => x)
              .Select(x => x.ToList())
              .Where(l => l.Zip(l.Skip(1), (t1, t2) => new { t1, t2 }).All(x => comparer(x.t1, x.t2))
                       && comparer(l.Last(), l.First()))
              .Distinct(new CircleEqualityComparer<T>());
    }
    
    public static IEnumerable<IEnumerable<T>> Permutations<T>(this IEnumerable<T> list, int length)
    {
        if (length == 1)
            return list.Select(t => new[] { t });
    
        return Permutations(list, length - 1)
              .SelectMany(t => list.Where(e => !t.Contains(e)), (t1, t2) => t1.Concat(new[] { t2 }));
    }
    
    public class CircleEqualityComparer<T> : IEqualityComparer<ICollection<T>>
    {
        public bool Equals(ICollection<T> x, ICollection<T> y)
        {
            if (x.Count != y.Count)
                return false;
    
            return Enumerable.Range(1, x.Count)
                  .Any(i => x.SequenceEqual(y.Skip(i).Concat(y.Take(i))));
        }
    
        public int GetHashCode(ICollection<T> obj)
        {
            return unchecked(obj.Aggregate(0, (x, y) => x + y.GetHashCode()));
        }
    }
    

    很少有事情需要注意:

    1. 我使用了ISet<T>HashSet<T>来代替更传统的List<T>,但这只是为了让意图更清晰,没有重复的项目是允许。列表应该可以正常工作。

    2. .NET确实没有插入顺序保留集(即不允许重复),因此必须在许多地方使用List<T>。一组可能会略微提高性能,但更重要的是可以互换地使用set和list导致混淆。

    3. 第一种方法在第二种方法上提供了100次的性能跳跃。

    4. 可以使用Prepare方法加快第二种方法。逻辑也存在,即收集中较小的成员意味着产生较少的排列。但仍然非常非常痛苦。

    5. 我已经使方法通用,但解决方案可以更通用。例如,在我的情况下,基于某个比较逻辑来检测循环。这可以作为参数传递,即,集合中的项目不需要仅仅是可比较的,它可以是任何顶点确定逻辑。但是这留给了读者&#39;锻炼。

    6. 在我的代码(两个示例)中,仅考虑最小尺寸3的循环,即,像A>的循环; B> C>答:它没有考虑像A&gt;这样的周期。 B,B>一种情况。如果您需要它,请将代码中3的所有实例更改为您喜欢的任何内容。更好的是将它传递给函数。