【问题标题】:Find Circular Items in a Collection (of non-Transitive Items)在集合中查找循环项(非传递项)
【发布时间】:2015-09-12 18:40:39
【问题描述】:

问题:

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

A
B
C
D
E

其中 A > BB > CC > A。也可能有像 A > B, B > C, C > DD > A em>,即不一定是 3 个组。我想要的是在给定的List<T> 中找到所有循环伟大组。例如,假设 A > B > C > AA > B > C > D > A 是上述情况下的两个循环组,我的输出应该看起来像:

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 > B > C > D (这相当慢,但我可以接受)

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

代码:

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 万个项目)。有没有更好的方法是相当有效的?我不追求最快的代码。这里有更好的递归方法吗?闻起来很香。

【问题讨论】:

    标签: c# .net linq recursion collections


    【解决方案1】:

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

    基本思路如下:

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

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

    3. 我忽略了真正糟糕的(即集合中没有较小的项目)和真正好的(即集合中没有更大的项目)以避免无限递归并通常提高性能.绝对最好的和最差的对周期没有贡献。全部在Prepare 方法中完成。

    4. 现在每个项目都会有一个小于该项目的项目集合。并且集合中的项目将拥有自己的较差项目集合。等等。这是我递归遍历的路径。

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

    6. 循环被添加到HashSet&lt;T&gt; 以避免重复。定义了一个相等比较器来检测等价的循环列表。

    代码:

    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&lt;T&gt; 约束,任何IComparable&lt;T&gt; 都应该工作。 不利的一面,它像一月份的糖蜜一样缓慢。

    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&lt;T&gt;s 和HashSet&lt;T&gt;s 而不是更传统的List&lt;T&gt;s,但这只是为了使意图更明确,不允许重复项。列表应该可以正常工作。

    2. .NET 并没有真正的插入顺序保留集(即不允许重复),因此不得不在许多地方使用List&lt;T&gt;。集合可能稍微提高了性能,但更重要的是,交替使用集合和列表会导致混淆。

    3. 第一种方法的性能比第二种方法高出 100 倍。

    4. 第二种方法可以通过使用Prepare 方法来加速。逻辑也成立,即集合中的较少成员意味着要生成的排列较少。但仍然非常非常缓慢。

    5. 我已将方法设为通用,但解决方案可以更通用。例如,在我的情况下,循环是基于某种比较逻辑来检测的。这可以作为参数传递,即集合中的项目不必只是可比较的,它可以是任何顶点确定逻辑。但这留给读者自己练习。

    6. 在我的代码(两个示例)中,仅考虑最小大小为 3 的循环,即像 A > B > C > A 这样的循环。它不考虑像 A > B、B > A 这样的循环情况。如果您需要它,请将代码中3 的所有实例更改为您喜欢的任何内容。最好将它传递给函数。

    【讨论】:

      【解决方案2】:

      场景...

      [A、B、C、D、E]

      其中 A > B, B > C, C > D, C > A, D > A

      ...可以使用A -&gt; B 表示A &gt; B 的约定表示为有向图:

      所以问题本质上是“如何在有向图中找到循环?”

      要解决这个问题,您可以使用Tarjan's strongly connected components algorithm。我建议您查找该算法的良好实现并将其应用到您的场景中。

      【讨论】:

        【解决方案3】:

        有许多方法可以枚举 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 倍。

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

        【讨论】:

        • 我有几个问题:这里的_cities 是什么,你从哪里得到它?你返回的浮点数是多少?
        猜你喜欢
        • 1970-01-01
        • 2023-03-24
        • 1970-01-01
        • 2020-04-06
        • 2018-01-30
        • 2011-09-23
        • 1970-01-01
        • 1970-01-01
        • 2019-06-18
        相关资源
        最近更新 更多