使用 LINQ 生成排列

Posted

技术标签:

【中文标题】使用 LINQ 生成排列【英文标题】:Generating Permutations using LINQ 【发布时间】:2011-05-18 04:05:58 【问题描述】:

我有一组必须安排的产品。有 P 个产品,每个产品的索引从 1 到 P。每个产品可以安排在 0 到 T 的时间段内。我需要构造满足以下约束的产品时间表的所有排列:

If p1.Index > p2.Index then p1.Schedule >= p2.Schedule.

我正在努力构建迭代器。当产品数量是已知常量时,我​​知道如何通过 LINQ 执行此操作,但不确定当产品数量是输入参数时如何生成此查询。

理想情况下,我想使用 yield 语法来构造这个迭代器。

public class PotentialSchedule()

      public PotentialSchedule(int[] schedulePermutation)
      
             _schedulePermutation = schedulePermutation;
      
      private readonly int[] _schedulePermutation;



private int _numberProducts = ...;
public IEnumerator<PotentialSchedule> GetEnumerator()

     int[] permutation = new int[_numberProducts];
     //Generate all permutation combinations here -- how?
     yield return new PotentialSchedule(permutation);

编辑:_numberProducts = 2 时的示例

public IEnumerable<PotentialSchedule> GetEnumerator()

    var query = from p1 in Enumerable.Range(0,T)
                from p2 in Enumerable.Range(p2,T)
                select new  P1 = p1, P2 = p2;

    foreach (var result in query)
          yield return new PotentialSchedule(new int[]  result.P1, result.P2 );

【问题讨论】:

看***.com/questions/774457/… 您意识到这会很快变得非常大,对吗?你介意我问为什么你需要生成所有可能的时间表吗? 是的——我意识到这将很快变得非常大。这是优化程序的一部分。最棘手的部分是扩展正在考虑的可能性。在最坏的情况下,我的问题大小大约是 P 第二个例子的第四行应该是(p1, T),是吗? 请注意,此类项目的数量等于 T 边的 P 维金字塔格子中的节点数。(因此,如果 P 为 3,T 为 5,则为点数一个最长边有五个点的三维金字塔。)一个边上有 15 个点的 20 维金字塔有 大量 个点。那个东西的点数和宇宙中基本粒子的数量差不多,(非常)粗略的近似。 【参考方案1】:

如果我理解这个问题:您正在寻找长度为 P 的所有整数序列,其中集合中的每个整数都介于 0 和 T 之间,并且该序列是单调不减。对吗?

使用迭代器块编写这样的程序很简单:

using System;
using System.Collections.Generic;
using System.Linq;

static class Program

    static IEnumerable<T> Prepend<T>(T first, IEnumerable<T> rest)
    
        yield return first;
        foreach (var item in rest)
            yield return item;
    

    static IEnumerable<IEnumerable<int>> M(int p, int t1, int t2)
    
        if (p == 0)
            yield return Enumerable.Empty<int>();
        else
            for (int first = t1; first <= t2; ++first)
                foreach (var rest in M(p - 1, first, t2))
                    yield return Prepend(first, rest);
    

    public static void Main()
    
        foreach (var sequence in M(4, 0, 2))
            Console.WriteLine(string.Join(", ", sequence));
    

这会产生所需的输出:从 0 到 2 绘制的长度为 4 的非递减序列。

0, 0, 0, 0
0, 0, 0, 1
0, 0, 0, 2
0, 0, 1, 1
0, 0, 1, 2
0, 0, 2, 2
0, 1, 1, 1
0, 1, 1, 2
0, 1, 2, 2
0, 2, 2, 2
1, 1, 1, 1
1, 1, 1, 2
1, 1, 2, 2
1, 2, 2, 2
2, 2, 2, 2

请注意,用于连接的多重嵌套迭代器的用法是not very efficient,但谁在乎呢?您已经在生成 指数 数量的序列,因此生成器中存在 多项式 效率低下的事实基本上是无关紧要的。

方法 M 生成长度为 p 的所有单调非递减整数序列,其中整数介于 t1 和 t2 之间。它使用简单的递归递归地执行此操作。基本情况是只有一个长度为零的序列,即空序列。递归情况是,为了计算,比如说 P = 3, t1 = 0, t2 = 2,你计算:

- all sequences starting with 0 followed by sequences of length 2 drawn from 0 to 2.
- all sequences starting with 1 followed by sequences of length 2 drawn from 1 to 2.
- all sequences starting with 2 followed by sequences of length 2 drawn from 2 to 2.

结果就是这样。

或者,您可以在主递归方法中使用查询理解而不是迭代器块:

static IEnumerable<T> Singleton<T>(T first)

    yield return first;


static IEnumerable<IEnumerable<int>> M(int p, int t1, int t2)

    return p == 0 ?

        Singleton(Enumerable.Empty<int>()) : 

        from first in Enumerable.Range(t1, t2 - t1 + 1)
        from rest in M(p - 1, first, t2)
        select Prepend(first, rest);

基本上做同样的事情;它只是将循环移动到 SelectMany 方法中。

【讨论】:

我更喜欢查询理解语法以提高可读性——与 yield 实现相比会有什么明显的缺陷吗? @erash:两者的效率都不是特别高或低。这几乎是一个偏好问题。 如果序列元素的范围是从0到k,并且序列的长度是n,那么输出中的序列数由这个公式给出: (n + (k − 1) )! / (k - 1)! /n!通过搜索“重复组合”可以找到许多证明,例如在这篇***文章en.wikipedia.org/wiki/Stars_and_bars_%28combinatorics%29【参考方案2】:

我使用这个库进行组合,发现它运行良好。示例程序有点混乱,但文章解释了使用代码需要什么。

使用 C# 泛型的排列、组合和变体 阿德里安·阿克森 | 2008 年 5 月 23 日 讨论六种主要类型的组合集合,并提供示例和计数公式。扩展了一组基于 C# 泛型的类,用于枚举每个元集合。 从http://www.codeproject.com/KB/recipes/Combinatorics.aspx插入

【讨论】:

【参考方案3】:

注意:Comparer 完全是可选的。如果您提供一个,则排列将按词法顺序返回。如果你不这样做,但原始项目是有序的,它仍然会按词汇顺序枚举。 Ian Griffiths 6 年前玩过这个,使用了一个更简单的算法(据我所知,它不进行词汇排序):http://www.interact-sw.co.uk/iangblog/2004/09/16/permuterate。

请记住,此代码已有几年历史,并且针对 .NET 2.0,因此没有扩展方法等(但修改起来应该很简单)。

它使用Knuth calls "Algorithm L"的算法。它是非递归的、快速的,用于 C++ 标准模板库。

static partial class Permutation

    /// <summary>
    /// Generates permutations.
    /// </summary>
    /// <typeparam name="T">Type of items to permute.</typeparam>
    /// <param name="items">Array of items. Will not be modified.</param>
    /// <param name="comparer">Optional comparer to use.
    /// If a <paramref name="comparer"/> is supplied, 
    /// permutations will be ordered according to the 
    /// <paramref name="comparer"/>
    /// </param>
    /// <returns>Permutations of input items.</returns>
    public static IEnumerable<IEnumerable<T>> Permute<T>(T[] items, IComparer<T> comparer)
    
        int length = items.Length;
        IntPair[] transform = new IntPair[length];
        if (comparer == null)
        
            //No comparer. Start with an identity transform.
            for (int i = 0; i < length; i++)
            
                transform[i] = new IntPair(i, i);
            ;
        
        else
        
            //Figure out where we are in the sequence of all permutations
            int[] initialorder = new int[length];
            for (int i = 0; i < length; i++)
            
                initialorder[i] = i;
            
            Array.Sort(initialorder, delegate(int x, int y)
            
                return comparer.Compare(items[x], items[y]);
            );
            for (int i = 0; i < length; i++)
            
                transform[i] = new IntPair(initialorder[i], i);
            
            //Handle duplicates
            for (int i = 1; i < length; i++)
            
                if (comparer.Compare(
                    items[transform[i - 1].Second], 
                    items[transform[i].Second]) == 0)
                
                    transform[i].First = transform[i - 1].First;
                
            
        

        yield return ApplyTransform(items, transform);

        while (true)
        
            //Ref: E. W. Dijkstra, A Discipline of Programming, Prentice-Hall, 1997
            //Find the largest partition from the back that is in decreasing (non-icreasing) order
            int decreasingpart = length - 2;
            for (;decreasingpart >= 0 && 
                transform[decreasingpart].First >= transform[decreasingpart + 1].First;
                --decreasingpart) ;
            //The whole sequence is in decreasing order, finished
            if (decreasingpart < 0) yield break;
            //Find the smallest element in the decreasing partition that is 
            //greater than (or equal to) the item in front of the decreasing partition
            int greater = length - 1;
            for (;greater > decreasingpart && 
                transform[decreasingpart].First >= transform[greater].First; 
                greater--) ;
            //Swap the two
            Swap(ref transform[decreasingpart], ref transform[greater]);
            //Reverse the decreasing partition
            Array.Reverse(transform, decreasingpart + 1, length - decreasingpart - 1);
            yield return ApplyTransform(items, transform);
        
    

    #region Overloads

    public static IEnumerable<IEnumerable<T>> Permute<T>(T[] items)
    
        return Permute(items, null);
    

    public static IEnumerable<IEnumerable<T>> Permute<T>(IEnumerable<T> items, IComparer<T> comparer)
    
        List<T> list = new List<T>(items);
        return Permute(list.ToArray(), comparer);
    

    public static IEnumerable<IEnumerable<T>> Permute<T>(IEnumerable<T> items)
    
        return Permute(items, null);
    

    #endregion Overloads

    #region Utility

    public static IEnumerable<T> ApplyTransform<T>(
        T[] items, 
        IntPair[] transform)
    
        for (int i = 0; i < transform.Length; i++)
        
            yield return items[transform[i].Second];
        
    

    public static void Swap<T>(ref T x, ref T y)
    
        T tmp = x;
        x = y;
        y = tmp;
    

    public struct IntPair
    
        public IntPair(int first, int second)
        
            this.First = first;
            this.Second = second;
        
        public int First;
        public int Second;
    

    #endregion


class Program


    static void Main()
    
        int pans = 0;
        int[] digits = new int[]  1, 2, 3, 4, 5, 6, 7, 8, 9 ;
        Stopwatch sw = new Stopwatch();
        sw.Start();
        foreach (var p in Permutation.Permute(digits))
        
            pans++;
            if (pans == 720) break;
        
        sw.Stop();
        Console.WriteLine("0pcs, 1ms", pans, sw.ElapsedMilliseconds);
        Console.ReadKey();
    

【讨论】:

【参考方案4】:
    创建另一个长度为 2^n 的数组,其中 n 是产品数 从 0 到 2^n 以二进制计数,并用每个计数填充数组。例如,如果 n=3 数组将如下所示:

000 001 010 011 100 101 110 111

    循环遍历二进制数组并在每个数字中找到一个,然后添加具有相同索引的产品:
 for each binaryNumber in ar
   for i = 0 to n-1
     if binaryNumber(i) = 1
       permunation.add(products(i))
   
  permunations.add(permutation) 

示例: 如果 binaryNumber= 001 那么 permunation1 = product1 如果 binaryNumber= 101 那么 permunation1 = product3,product1

【讨论】:

【参考方案5】:

这是 C# 7 的简单排列扩展方法(值元组和内部方法)。它源自@AndrasVaas's answer,但只使用了单一级别的惰性(防止由于项目随着时间的推移而发生的错误),失去了IComparer 功能(我不需要它),并且相当短。

public static class PermutationExtensions

    /// <summary>
    /// Generates permutations.
    /// </summary>
    /// <typeparam name="T">Type of items to permute.</typeparam>
    /// <param name="items">Array of items. Will not be modified.</param>
    /// <returns>Permutations of input items.</returns>
    public static IEnumerable<T[]> Permute<T>(this T[] items)
    
        T[] ApplyTransform(T[] values, (int First, int Second)[] tx)
        
            var permutation = new T[values.Length];
            for (var i = 0; i < tx.Length; i++)
                permutation[i] = values[tx[i].Second];
            return permutation;
        

        void Swap<U>(ref U x, ref U y)
        
            var tmp = x;
            x = y;
            y = tmp;
        

        var length = items.Length;

        // Build identity transform
        var transform = new(int First, int Second)[length];
        for (var i = 0; i < length; i++)
            transform[i] = (i, i);

        yield return ApplyTransform(items, transform);

        while (true)
        
            // Ref: E. W. Dijkstra, A Discipline of Programming, Prentice-Hall, 1997
            // Find the largest partition from the back that is in decreasing (non-increasing) order
            var decreasingpart = length - 2;
            while (decreasingpart >= 0 && transform[decreasingpart].First >= transform[decreasingpart + 1].First)
                --decreasingpart;

            // The whole sequence is in decreasing order, finished
            if (decreasingpart < 0)
                yield break;

            // Find the smallest element in the decreasing partition that is
            // greater than (or equal to) the item in front of the decreasing partition
            var greater = length - 1;
            while (greater > decreasingpart && transform[decreasingpart].First >= transform[greater].First)
                greater--;

            // Swap the two
            Swap(ref transform[decreasingpart], ref transform[greater]);

            // Reverse the decreasing partition
            Array.Reverse(transform, decreasingpart + 1, length - decreasingpart - 1);

            yield return ApplyTransform(items, transform);
        
    

【讨论】:

【参考方案6】:

今天我偶然发现了这一点,并认为我可以分享我的实现。

对于 N 和 M 之间的所有整数,您必须先创建一个数组:

IEnumerable<int> Range(int n, int m) 
    for(var i = n; i < m; ++i) 
        yield return i;
    

并通过Permutations(Range(1, 10))运行它:

    enum PermutationsOption 
        None,
        SkipEmpty,
        SkipNotDistinct
    

    private IEnumerable<IEnumerable<T>> Permutations<T>(IEnumerable<T> elements, PermutationsOption option = PermutationsOption.None, IEqualityComparer<T> equalityComparer = default(IEqualityComparer<T>)) 
        var elementsList = new List<IEnumerable<T>>();
        var elementsIndex = 0;
        var elementsCount = elements.Count();
        var elementsLength = Math.Pow(elementsCount + 1, elementsCount);

        if (option.HasFlag(PermutationsOption.SkipEmpty)) 
            elementsIndex = 1;
        

        if (elements.Count() > 0) 
            do 
                var elementStack = new Stack<T>();

                for (var i = 0; i < elementsCount; ++i) 
                    var ind = (int)(elementsIndex / Math.Pow(elementsCount + 1, i) % (elementsCount + 1));
                    if (ind == 0) 
                        continue;
                    
                    elementStack.Push(elements.ElementAt(ind - 1));
                

                var elementsCopy = elementStack.ToArray() as IEnumerable<T>;

                if (option.HasFlag(PermutationsOption.SkipNotDistinct)) 
                    elementsCopy = elementsCopy.Distinct();
                    elementsCopy = elementsCopy.ToArray();

                    if (elementsList.Any(p => CompareItemEquality(p, elementsCopy, equalityComparer))) 
                        continue;
                    
                

                elementsList.Add(elementsCopy);
             while (++elementsIndex < elementsLength);
        

        return elementsList.ToArray();
    

    private bool CompareItemEquality<T>(IEnumerable<T> elements1, IEnumerable<T> elements2, IEqualityComparer<T> equalityComparer = default(IEqualityComparer<T>)) 
        if (equalityComparer == null) 
            equalityComparer = EqualityComparer<T>.Default;
        

        return (elements2.Count() == elements2.Count()) && (elements2.All(p => elements1.Contains(p, equalityComparer)));
    

【讨论】:

【参考方案7】:

Lippert 先生的答案的输出可以看作是元素在 4 个槽中 0 和 2 之间的所有可能分布。 比如 0 3 1 读作“没有 0,三个 1 和一个 2” 这远没有利珀特先生的回答那么优雅,但至少效率不低

public static void Main()

  var distributions = Distributions(4, 3);
  PrintSequences(distributions);


/// <summary>
/// Entry point for the other recursive overload
/// </summary>
/// <param name="length">Number of elements in the output</param>
/// <param name="range">Number of distinct values elements can take</param>
/// <returns></returns>
static List<int[]> Distributions(int length, int range)

  var distribution = new int[range];
  var distributions = new List<int[]>();
  Distributions(0, length, distribution, 0, distributions);
  distributions.Reverse();
  return distributions;


/// <summary>
/// Recursive methode. Not to be called directly, only from other overload
/// </summary>
/// <param name="index">Value of the (possibly) last added element</param>
/// <param name="length">Number of elements in the output</param>
/// <param name="distribution">Distribution among element distinct values</param>
/// <param name="sum">Exit condition of the recursion. Incremented if element added from parent call</param>
/// <param name="distributions">All possible distributions</param>
static void Distributions(int index,
                          int length,
                          int[] distribution,
                          int sum,
                          List<int[]> distributions)

  //Uncomment for exactness check
  //System.Diagnostics.Debug.Assert(distribution.Sum() == sum);
  if (sum == length)
  
    distributions.Add(distribution.Reverse().ToArray());

    for (; index < distribution.Length; index++)
    
      sum -= distribution[index];
      distribution[index] = 0;
    
    return;
  
  if (index < distribution.Length)
  
    Distributions(index + 1, length, distribution, sum, distributions);
    distribution[index]++;
    Distributions(index, length, distribution, ++sum, distributions);
  


static void PrintSequences(List<int[]> distributions)

  for (int i = 0; i < distributions.Count; i++)
  
    for (int j = distributions[i].Length - 1; j >= 0; j--)
      for (int k = 0; k < distributions[i][j]; k++)
        Console.Write("0:D1 ", distributions[i].Length - 1 - j);
    Console.WriteLine();
  

【讨论】:

【参考方案8】:
    public static IList<IList<T>> Permutation<T>(ImmutableList<ImmutableList<T>> dimensions)
    
        IList<IList<T>> result = new List<IList<T>>();
        Step(ImmutableList.Create<T>(), dimensions, result);
        return result;
    

    private static void Step<T>(ImmutableList<T> previous, 
        ImmutableList<ImmutableList<T>> rest, 
        IList<IList<T>> result)
    
        if (rest.IsEmpty)
        
            result.Add(previous);
            return;
        

        var first = rest[0];
        rest = rest.RemoveAt(0);

        foreach (var label in first)
        
            Step(previous.Add(label), rest, result);
        
    

【讨论】:

以上是关于使用 LINQ 生成排列的主要内容,如果未能解决你的问题,请参考以下文章

如何使用LINQ中的条件重新排列选择结果

使用 linq 生成直接更新,无需选择

Linq操作XML生成XML,实体生成XML和互转

使用 SQLMetal 从 SQL Compact 3.5 数据库生成 Linq 类

Linq 中的组合生成器

C#中LINQ怎么生成下面那条SQL