生成所有可能的组合

72

给定2个数组 Array1 = {a,b,c...n}Array2 = {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)

4
您能否给出一个更好的例子,使用更少的元素并产生完整的结果?例如,我有一个问题是第一个数组的每个元素是否仅应与第二个数组的对应元素配对,还是您想将其与第二个数组的所有元素组合在一起。 - Lasse V. Karlsen
可能数组的大小是相同的。 - Gulshan
是的,两个数组的大小相同。 - Amitd
8
Eric专门为您撰写了这篇博客 :) http://blogs.msdn.com/b/ericlippert/archive/2010/06/28/computing-a-cartesian-product-with-linq.aspx - Ahmed
@Ahmed提到的帖子的更新链接:https://ericlippert.com/2010/06/28/computing-a-cartesian-product-with-linq/ - undefined
12个回答

168
当然可以。使用标准查询运算符,虽然使用LINQ有点棘手,但确实可以做到。
更新:我的博客于2010年6月28日星期一就此问题发表了文章,感谢这个好问题的提出者。此外,我博客上的评论者指出,有比我给出的更优雅的查询。我将在此处更新代码以使用它。
棘手的部分是使任意多个序列的笛卡尔积。与字母“zipping”相比,这很微不足道。您应该学习这个,以确保理解它的工作原理。每个部分都很简单,但它们如何组合在一起需要一些时间来适应:
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)

通过反复将累加器与输入序列中的下一个项进行笛卡尔积,并对结果进行一些粘贴,我们可以在执行过程中生成笛卡尔积。
因此,请考虑累加器的值。为了说明问题,我将展示累加器的值作为其包含的序列运算符的结果。这不是累加器实际包含的内容。累加器实际包含的是产生这些结果的运算符。整个操作只是构建了一个巨大的序列运算符树,其结果是笛卡尔积。但是,直到查询执行时,最终的笛卡尔积本身才没有实际计算。为了说明问题,在每个阶段显示结果,但请记住,其中实际包含产生这些结果的运算符。
假设我们正在对序列序列 {{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();
}

轻而易举!


9
@FlorisDevriendt:不用谢。你已经发现为什么尝试制作一个“演示问题的小完整示例”是个好主意了。这样做通常可以解决问题。另一种行之有效的技巧是获得一只橡皮鸭,然后向它大声解释问题的确切内容。通过这样做,很常见能够找到答案,无论这只鸭子是否知道答案。 - Eric Lippert
@EricLippert,这似乎是一个完美的解释和优秀的解决方案,但在我确认理解之前,我需要更深入地探究一下。同时... 你知道两件事吗:1 - 这个解决方案是否递归,可能会快速创建一个堆栈溢出?2 - 它是否创建了一个非常大的可能性数组,也可能会成为一个内存问题?还是它使用了“最小状态内存”和“yield关键字”的组合,以保持内存占用率较低? - Eric Ouellet
1
@EricOuellet:(1)你可以判断这是否是递归的。每个递归函数都有相同的形式:首先处理一个基本情况,如果问题很简单,否则制造一个或多个更简单的问题,用递归调用解决它们,并将解决方案组合成更大问题的解决方案。这种方法是否遵循该模式? - Eric Lippert
@EricOuellet:显然,该方法本身并不是递归的。但是除了递归之外,还有其他导致堆栈溢出的方式。这里确实存在一个任意深度的调用堆栈,但它被很好地隐藏起来了。你能找到它吗 - Eric Lippert
1
@EricOuellet:对于笛卡尔积,组合数不是n!。相反,如果我们有大小为a、b、c、d...的序列,则组合数为a x b x c x d ... 这也会非常快地变得非常大!然而,在这种情况下,我们将发现递归最深的层数是n,其中n是要组合的序列数。因此,它是无限的,但很不可能甚至达到几十,更不用说成千上万了。 - Eric Lippert
显示剩余5条评论

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

13

替代解决方案:

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

链接

第二步:定义一个生成你想要的语言的语法。例如,你可以定义以下语法:

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

显然,你可以轻松地从这两个数组生成语法定义字符串。然后将其提供给生成给定语法中所有字符串的代码,就完成了; 你会得到所有可能性(不一定是你想要的顺序)。


现在先不考虑顺序,但语法是否可以用于生成特定顺序的序列呢? - Amitd

7
使用.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)));
}

这是一个很棒的答案 - 使用方法组使得正在发生的事情非常清晰。 - NetMage
我想指出的是,它并不比原始答案更轻量级,因为 Enumerable.Append 方法创建了一个链接对象的链式列表来跟踪追加操作,这可能比只有一个元素的数组更耗费资源。 - NetMage

3

除了基于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;
        }
    }

您可以在这里找到关于如何使用它的例子。


当数组数量仅在运行时已知时,这对我非常有效! - Simon Arsenault

2
我不愿意给你完整的源代码。所以这是它背后的想法。
你可以按以下方式生成元素:
我假设 A=(a1,a2,...,an) 和 B=(b1,b2,...,bn) (因此 A 和 B 每个都有 n 个元素)。
然后递归地执行!编写一个方法,接受 A 和 B 并进行操作:
如果 A 和 B 各自只包含一个元素(分别称为 an 和 bn),则从 1 到 bn 迭代,并将 an 连接到您的迭代变量。
如果 A 和 B 各自包含多个元素,则获取第一个元素(a1 和 b1),从 1 到 bn 迭代,并对于每个迭代步骤执行:
使用AB的子字段递归地调用方法,从第二个元素开始,即A'=(a2, a3, ..., an)B'=(b2, b3, ..., bn)。对于由递归调用生成的每个元素,连接a1,迭代变量和递归调用生成的元素。在这里,您可以找到一个类似的C#示例,您只需要根据自己的需求进行调整。

1

另一种不基于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]++;
        }
    }
}

虽然这个答案可能是正确和有用的,但最好还是附上一些解释,以解释它如何帮助解决问题。如果将来发生了变化(可能与此无关),导致它停止工作并且用户需要了解它曾经如何工作,这将变得尤其有用。 - Kevin Brown-Silva

1

如果我理解正确,您想要类似于笛卡尔积的东西。 如果是这种情况,以下是您可以使用LINQ实现此操作的方法。可能不是完全准确的答案,但尝试理解其思路。


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

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

这些文章可能会有所帮助


不,他可能的“组合”(在我看来,OP选择了一个不好的词)并不仅仅是两个集合的笛卡尔积。第二个数组定义了一个上限,而不是实际值。 - Adam Robinson

1

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} {1}{2}",a,Array1[i],b).Trim();
    finalResult = tmp.ToList();
}

我认为这就足够了。


0

如果有人对笛卡尔积算法的工业化、测试和支持实现感兴趣,欢迎使用现成的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())

该软件包提供了一个自动计划生成器,可以优化掉这些特殊情况。因此,两种方法具有相同的计算复杂度。
该软件包的相应源代码比片段大一些,但可在GitHub上获得。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接