生成所有可能的组合

Posted

技术标签:

【中文标题】生成所有可能的组合【英文标题】:Generating all Possible Combinations 【发布时间】:2022-01-10 14:01:26 【问题描述】:

给定 2 个数组 Array1 = a,b,c...nArray2 = 10,20,15....x 我如何将所有可能的组合生成为字符串 a(i) b(j) c(k) n(p)

1 <= i <= 10,  1 <= j <= 20 , 1 <= k <= 15,  .... 1 <= p <= x

如:

a1 b1 c1 .... n1  
a1 b1 c1..... n2  
......  
......  
a10 b20 c15 nx (last combination)

所以在所有组合总数中 = array2 = (10 X 20 X 15 X ..X x) 元素的乘积

类似于笛卡尔积,其中第二个数组定义了第一个数组中每个元素的上限。

以固定数字为例,

    Array x =  [a,b,c]
    Array y =  [3,2,4] 

所以我们将有 3*2*4 = 24 种组合。结果应该是:

    a1 b1 c1  
    a1 b1 c2  
    a1 b1 c3  
    a1 b1 c4  

    a1 b2 c1  
    a1 b2 c2  
    a1 b2 c3  
    a1 b2 c4


    a2 b1 c1  
    a2 b1 c2  
    a2 b1 c3  
    a2 b1 c4  

    a2 b2 c1  
    a2 b2 c2  
    a2 b2 c3  
    a2 b2 c4


    a3 b1 c1  
    a3 b1 c2  
    a3 b1 c3  
    a3 b1 c4  

    a3 b2 c1  
    a3 b2 c2  
    a3 b2 c3  
    a3 b2 c4 (last)

【问题讨论】:

你能举一个更好的例子,使用更少的元素,并产生完整的结果吗?例如,我的一个问题是第一个数组的每个元素是否应该只与第二个数组的相应元素配对,或者是否要将它与第二个数组的所有元素组合。 可能数组的大小是一样的。 是的 2 个数组大小相同.. Eric 专门为你写了这篇博客 :) blogs.msdn.com/b/ericlippert/archive/2010/06/28/… 【参考方案1】:

当然。使用 LINQ 执行此操作有点棘手,但肯定可以仅使用标准查询运算符。

更新:这是my blog on Monday June 28th 2010 的主题;谢谢你的好问题。此外,我博客上的一位评论者指出,有一个比我给出的更优雅的查询。我将在此处更新代码以使用它。

棘手的部分是制作任意多个序列的笛卡尔积。与此相比,字母中的“压缩”是微不足道的。你应该研究这个以确保你理解它是如何工作的。每个部分都很简单,但它们组合在一起的方式需要一些时间来适应:

static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences)

    IEnumerable<IEnumerable<T>> emptyProduct = new[]  Enumerable.Empty<T>();
    return sequences.Aggregate(
        emptyProduct,
        (accumulator, sequence) => 
            from accseq in accumulator 
            from item in sequence 
            select accseq.Concat(new[] item)                          
        );
 

要解释它是如何工作的,首先要了解“累积”操作在做什么。最简单的累加操作是“将这个序列中的所有内容加在一起”。你这样做的方式是:从零开始。对于序列中的每一项,累加器的当前值等于该项与累加器先前值的总和。我们正在做同样的事情,只是我们不是根据到目前为止的总和和当前项目来累加总和,而是在进行过程中累加笛卡尔积。

我们要这样做的方法是利用我们在 LINQ 中已经有一个运算符来计算两件事的笛卡尔积:

from x in xs
from y in ys
do something with each possible (x, y)

通过重复将累加器的笛卡尔积与输入序列中的下一项相乘并将结果粘贴在一起,我们可以随时生成笛卡尔积。

所以想想累加器的价值。出于说明目的,我将把累加器的值显示为它所包含的序列运算符的 results。这不是累加器实际上包含的内容。累加器实际包含的是产生这些结果的运算符。这里的整个操作只是建立了一个序列运算符的大量树,其结果是笛卡尔积。但是直到执行查询时才真正计算最终的笛卡尔积本身。出于说明目的,我将展示每个阶段的结果,但请记住,这实际上包含产生这些结果的运算符

假设我们正在取序列1, 2, 3, 4, 5, 6 的笛卡尔积。累加器以包含一个空序列的序列开始:

第一次累加时,累加器为 ,项目为 1, 2。我们这样做:

from accseq in accumulator
from item in sequence 
select accseq.Concat(new[] item)

所以我们将 1, 2 的笛卡尔积相乘,对于每一对,我们连接:我们有( , 1),所以我们连接 1 得到1 .我们有一对( , 2),所以我们连接 2得到2。因此我们得到1, 2 作为结果。

所以在第二次累加时,累加器是1, 2,项目是3, 4。同样,我们计算这两个序列的笛卡尔积得到:

 (1, 3), (1, 4), (2, 3), (2, 4)

然后从这些项目中,将第二个连接到第一个。所以结果就是序列1, 3, 1, 4, 2, 3, 2, 4,这就是我们想要的。

现在我们再次积累。我们用5, 6取累加器的笛卡尔积得到

 ( 1, 3, 5), (1, 3, 6), (1, 4, 5), ...

然后将第二个项目连接到第一个得到:

1, 3, 5, 1, 3, 6, 1, 4, 5, 1, 4, 6 ... 

我们完成了。我们已经积累了笛卡尔积。

现在我们有了一个可以取任意多个序列的笛卡尔积的实用函数,剩下的比较容易:

var arr1 = new[] "a", "b", "c";
var arr2 = new[]  3, 2, 4 ;
var result = from cpLine in CartesianProduct(
                 from count in arr2 select Enumerable.Range(1, count)) 
             select cpLine.Zip(arr1, (x1, x2) => x2 + x1);

现在我们有一个字符串序列,每行一个字符串序列:

foreach (var line in result)

    foreach (var s in line)
        Console.Write(s);
    Console.WriteLine();

简单易懂!

【讨论】:

@FlorisDevriendt:不客气。您已经发现为什么尝试制作一个演示问题的小型完整示例是一个好主意。 这样做通常可以解决问题。 另一种有效的方法是获得橡皮鸭,然后大声向橡皮鸭解释确切的问题。这样做很容易发现答案,不管鸭子是否知道。 @EricLippert,这似乎是一个完美的解释和出色的解决方案,但我必须更深入地探索一下,然后才能说我明白了。与此同时......你知道两件事吗: 1 - 解决方案是否是递归的,可以快速创建堆栈溢出? 2 - 它是否会产生大量可能也是内存问题的可能性?还是它使用“内存中的最少状态”和“yield 关键字”的组合来保持低内存? @EricOuellet:(1)您可以确定这是否是递归的。每个递归函数都具有相同的形式:如果问题简单,则首先处理基本情况,否则生成一个或多个更简单的问题,通过递归调用解决它们,并将解决方案组合成更大问题的解决方案。 此方法是否遵循该模式? @EricOuellet:显然方法本身不是递归的。但是除了递归之外,还有其他产生堆栈溢出的方法。这里一个任意深度的调用栈,但它隐藏得很好。 你能找到它吗? @EricOuellet:关于(2),显然这分配了大量的对象来构建最终的积累。不过,它们都不是大数组;唯一构造的数组是单元素数组。 您能否将此解决方案创建的对象数量描述为输入大小的函数?输出怎么样?【参考方案2】:
using System;
using System.Text;

public static string[] GenerateCombinations(string[] Array1, int[] Array2)

    if(Array1 == null) throw new ArgumentNullException("Array1");
    if(Array2 == null) throw new ArgumentNullException("Array2");
    if(Array1.Length != Array2.Length)
        throw new ArgumentException("Must be the same size as Array1.", "Array2");

    if(Array1.Length == 0)
        return new string[0];

    int outputSize = 1;
    var current = new int[Array1.Length];
    for(int i = 0; i < current.Length; ++i)
    
        if(Array2[i] < 1)
            throw new ArgumentException("Contains invalid values.", "Array2");
        if(Array1[i] == null)
            throw new ArgumentException("Contains null values.", "Array1");
        outputSize *= Array2[i];
        current[i] = 1;
    

    var result = new string[outputSize];
    for(int i = 0; i < outputSize; ++i)
    
        var sb = new StringBuilder();
        for(int j = 0; j < current.Length; ++j)
        
            sb.Append(Array1[j]);
            sb.Append(current[j].ToString());
            if(j != current.Length - 1)
                sb.Append(' ');
        
        result[i] = sb.ToString();
        int incrementIndex = current.Length - 1;
        while(incrementIndex >= 0 && current[incrementIndex] == Array2[incrementIndex])
        
                current[incrementIndex] = 1;
                --incrementIndex;
        
        if(incrementIndex >= 0)
            ++current[incrementIndex];
    
    return result;

【讨论】:

【参考方案3】:

替代解决方案:

第一步:阅读我关于如何生成与上下文相关语法匹配的所有字符串的系列文章:

http://blogs.msdn.com/b/ericlippert/archive/tags/grammars/

第二步:定义生成所需语言的语法。例如,您可以定义语法:

S: a A b B c C
A: 1 | 2 | 3
B: 1 | 2
C: 1 | 2 | 3 | 4

显然,您可以轻松地从您的两个数组中生成该语法定义字符串。然后将其输入到生成给定语法中所有字符串的代码中,就完成了;你会得到所有的可能性。 (请注意,不一定按照您想要的顺序排列。)

【讨论】:

顺序现在不重要...但是可以使用语法来生成特定顺序的序列吗?【参考方案4】:

您可以使用另一种不基于 linq 的解决方案:

public class CartesianProduct<T>
    
        int[] lengths;
        T[][] arrays;
        public CartesianProduct(params  T[][] arrays)
        
            lengths = arrays.Select(k => k.Length).ToArray();
            if (lengths.Any(l => l == 0))
                throw new ArgumentException("Zero lenght array unhandled.");
            this.arrays = arrays;
        
        public IEnumerable<T[]> Get()
        
            int[] walk = new int[arrays.Length];
            int x = 0;
            yield return walk.Select(k => arrays[x++][k]).ToArray();
            while (Next(walk))
            
                x = 0;
                yield return walk.Select(k => arrays[x++][k]).ToArray();
            

        
        private bool Next(int[] walk)
        
            int whoIncrement = 0;
            while (whoIncrement < walk.Length)
            
                if (walk[whoIncrement] < lengths[whoIncrement] - 1)
                
                    walk[whoIncrement]++;
                    return true;
                
                else
                
                    walk[whoIncrement] = 0;
                    whoIncrement++;
                
            
            return false;
        
    

您可以在 how to use it here 上找到示例。

【讨论】:

当数组的数量只在运行时才知道时,这对我来说效果很好!【参考方案5】:

使用在.NET Framework 4.7.1 中添加的Enumerable.Append,@EricLippert 的答案可以在每次迭代时不分配新数组的情况下实现:

public static IEnumerable<IEnumerable<T>> CartesianProduct<T>
    (this IEnumerable<IEnumerable<T>> enumerables)

    IEnumerable<IEnumerable<T>> Seed()  yield return Enumerable.Empty<T>(); 

    return enumerables.Aggregate(Seed(), (accumulator, enumerable)
        => accumulator.SelectMany(x => enumerable.Select(x.Append)));

【讨论】:

【参考方案6】:

我不愿意给你完整的源代码。所以这就是背后的想法。

您可以通过以下方式生成元素:

我假设A=(a1, a2, ..., an)B=(b1, b2, ..., bn) (所以AB 各自持有n 元素)。

然后递归执行!编写一个采用AB 的方法并执行您的操作:

如果AB 都只包含一个元素(称为anbn),只需从1 迭代到bn 并将an 连接到您的迭代变量。

如果AB 都包含一个以上的元素,则获取第一个元素(a1 resp b1),从 1 迭代到 bn 并为每个迭代步骤执行操作:

使用从第二个元素开始的AB 的子字段递归调用该方法,即A'=(a2, a3, ..., an)B'=(b2, b3, ..., bn)。对于递归调用生成的每个元素,连接a1、迭代变量和递归调用生成的元素。

【讨论】:

【参考方案7】:

如果我猜对了,那么您就是 Cartesian product 之类的东西。 如果是这种情况,您可以使用 LINQ 执行此操作。可能不是确切的答案,但请尝试理解


    char[] Array1 =  'a', 'b', 'c' ;
    string[] Array2 =  "10", "20", "15" ;

    var result = from i in Array1
                 from j in Array2
                   select i + j;

这些文章可能会有所帮助

SelectMany

How to Use LINQ SelectMany

【讨论】:

不,他可能的“组合”(在 OP 方面选择不当,IMO)不仅仅是这两组的笛卡尔积。第二个数组定义了一个上限,而不是实际值。【参考方案8】:

finalResult 是所需的数组。假设两个数组大小相同。

char[] Array1 =  'a', 'b', 'c' ;
int[] Array2 =  3, 2, 4 ;

var finalResult = new List<string>();
finalResult.Add(String.Empty);
for(int i=0; i<Array1.Length; i++)

    var tmp = from a in finalResult
              from b in Enumerable.Range(1,Array2[i])
              select String.Format("0 12",a,Array1[i],b).Trim();
    finalResult = tmp.ToList();

我认为这就足够了。

【讨论】:

【参考方案9】:

另一种不基于 linq 的解决方案更有效:

static IEnumerable<T[]> CartesianProduct<T>(T[][] arrays) 
    int[] lengths;
    lengths = arrays.Select(a => a.Length).ToArray();
    int Len = arrays.Length;
    int[] inds = new int[Len];
    int Len1 = Len - 1;
    while (inds[0] != lengths[0]) 
        T[] res = new T[Len];
        for (int i = 0; i != Len; i++) 
            res[i] = arrays[i][inds[i]];
        
        yield return res;
        int j = Len1;
        inds[j]++;
        while (j > 0 && inds[j] == lengths[j]) 
            inds[j--] = 0;
            inds[j]++;
        
    

【讨论】:

虽然此答案可能是正确且有用的,但如果您 include some explanation along with it 解释它如何帮助解决问题,则最好。如果有更改(可能不相关)导致它停止工作并且用户需要了解它曾经是如何工作的,这在未来变得特别有用。【参考方案10】:

如果有人对笛卡尔积算法的工业化、测试和受支持的实现感兴趣,欢迎使用现成的Gapotchenko.FX.Math.Combinatorics NuGet 包。

它提供了两种操作模式。一种基于 LINQ 的流畅模式:

using Gapotchenko.FX.Math.Combinatorics;
using System;

foreach (var i in new[]  "1", "2" .CrossJoin(new[]  "A", "B", "C" ))
    Console.WriteLine(string.Join(" ", i));

还有一个显式模式,更加冗长:

using Gapotchenko.FX.Math.Combinatorics;
using System;

var seq1 = new[]  "1", "2" ;
var seq2 = new[]  "A", "B", "C" ;

foreach (var i in CartesianProduct.Of(seq1, seq2))
    Console.WriteLine(string.Join(" ", i));

两种模式产生相同的结果:

1 A
2 A
1 B
2 B
1 C
2 C

但它比这更进一步。例如,对ValueTuple 结果的投影是一个简单的单行:

var results = new[]  1, 2 .CrossJoin(new[]  "A", "B" , ValueTuple.Create);

foreach (var (a, b) in results)
  Console.WriteLine("0 1", a, b);

结果的唯一性可以通过自然的方式实现:

var results = new[]  1, 1, 2 .CrossJoin(new[]  "A", "B", "A" ).Distinct();

乍一看,这样的做法会产生过多的组合浪费。所以不要这样做

new[]  1, 1, 2 .CrossJoin(new[]  "A", "B", "A" ).Distinct()

在执行昂贵的乘法之前,Distinct() 序列可能更有益:

new[]  1, 1, 2 .Distinct().CrossJoin(new[]  "A", "B", "A" .Distinct())

该软件包提供了一个自动计划生成器,可以优化这些特质。因此,两种方法具有相同的计算复杂度。

包的相应源代码比 sn-p 可以包含的要大一点,但可以在 GitHub 获得。

【讨论】:

【参考方案11】:

这是一个 javascript 版本,我相信有人可以转换它。它已经过彻底的测试。

Here's the fiddle.

function combinations (Asource)

    var combos = [];
    var temp = [];

    var picker = function (arr, temp_string, collect) 
        if (temp_string.length) 
           collect.push(temp_string);
        

        for (var i=0; i<arr.length; i++) 
            var arrcopy = arr.slice(0, arr.length);
            var elem = arrcopy.splice(i, 1);

            if (arrcopy.length > 0) 
                picker(arrcopy, temp_string.concat(elem), collect);
             else 
                collect.push(temp_string.concat(elem));
               
           
    

    picker(Asource, temp, combos);

    return combos;



var todo = ["a", "b", "c", "d"]; // 5 in this set
var resultingCombos = combinations (todo);
console.log(resultingCombos);

【讨论】:

为什么会出现在这里?问题是关于 C#。 JavaScript 有自己的笛卡尔积线程,例如,Cartesian product of multiple arrays in JavaScript。这应该张贴在那里。

以上是关于生成所有可能的组合的主要内容,如果未能解决你的问题,请参考以下文章

使用递归和回溯生成所有可能的组合

[Python]:生成所有可能组合的数组

生成所有可能的组合 - Java [重复]

生成所有可能的组合

生成所有可能的组合

生成 0, 1,...n-1, n 个 k 数的所有可能组合。每个组合应按升序排列[重复]