如何使用递归收益生成所有替换组合? C#

时间:2015-12-17 00:32:02

标签: c# recursion yield

此处的目标是使用递归生成所有替换的组合,使其不超过RAM。屈服运算符就是为此而设计的。我想使用yield运算符,因为如果不这样做,我将超过可用的RAM。我合并的元素数量导致了数十亿的组合。

我决定如何使用递归yield生成所有组合而不用替换。以下是我写的例子:

public static IEnumerable<int[]> GetCombinationsWithYield(this int[] elements, int length)
{
   return Combinations2(elements, length, 0, new int[length]);
}

private static IEnumerable<int[]> Combinations2(int[] input, int len, int startPosition, int[] result)
{
    if (len == 0)
    {
        yield return result;
    }
    else
    {
        for (int i = startPosition; i <= input.Length - len; i++)
        {
            result[result.Length - len] = input[i];

            //  need to return the results of the recursive call
            foreach (var combination in Combinations2(input, len - 1, i + 1, result))
            {
                yield return combination;
            }
        }
    }
}

您可以使用以下单元测试进行测试:

[Test]
public void CombinationsUsingArraysOnlyWithYield()
{
    // use this method when RAM consumption is a concern.

    int[] items = {1, 2, 3};

    var results = new int[3][];
    for (int i = 0; i < results.Length; i++)
        results[i] = new int[2];

    int index = 0;

    var stopwatch = new Stopwatch();
    stopwatch.Start();

    // i only copy the results in to an array so that I don't benchmark Console.WriteLine stuff.
    // for this to be truly useful, you would not want to copy the results.
    foreach (var result in items.GetCombinationsWithYield(2))
    {
        Array.Copy(result, results[index], 2);
        index++;
    }

    stopwatch.Stop();

    for (int i = 0; i < results.GetLength(0); i++)
    {
        string output = "";
        for (int j = 0; j < results[i].Length; j++)
            output += results[i][j] + ",";
        Console.WriteLine(output);
    }

    Console.WriteLine("elapsed: " + stopwatch.ElapsedTicks + "[ticks]");
}

输出结果为:

1,2,
1,3,
2,3,
elapsed: 56597[ticks]

但正如您所看到的,该示例是,没有替换。

另一方面,我想使用替换,以便输出如下所示:

1,1,
1,2,
1,3,
2,1,
2,2,
2,3,
3,1,
3,2,
3,3,

我使用Linq解决方案实现了这一目标。但它没有利用yield运算符并溢出我的RAM。这是解决方案:

public static List<List<T>> GetCombinations<T>(this IEnumerable<T> pool, int comboLength, bool isWithReplacement)
{
    if (isWithReplacement)
        return GetCombinations(pool, comboLength).Select(c => c.ToList()).ToList();

}

private static IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> list, int length)
{
    if (length == 1) return list.Select(t => new[] {t});

    return GetCombinations(list, length - 1).SelectMany(t => list, (t1, t2) => t1.Concat(new[] {t2}));
}

非常感谢任何帮助。熟悉Knuth算法的人是理想的。

2 个答案:

答案 0 :(得分:2)

您正在使用的LINQ操作是使用迭代器块在内部实现的。您实质上是在寻求将这些操作内联到解决方案中的解决方案。 这会导致与当前解决方案完全相同的问题。这会导致您创建一个昂贵的状态机 ton ,它们都被丢弃而很少使用。为了避免极高的内存占用,您需要避免首先创建这么多状态机。编写递归迭代器块不会实现这一点。编写迭代而不是递归的解决方案(无论是否是迭代器块),都是实现这一目标的一种方法。

迭代解决方案非常简单,内存占用常量。您只需要计算所有组合的数量,然后为每个组合计算与该唯一索引的组合(这很简单)。

private static IEnumerable<IEnumerable<T>> GetCombinations<T>(IList<T> list, int length)
{
    var numberOfCombinations = (long)Math.Pow(list.Count, length);
    for(long i = 0; i < numberOfCombinations; i++)
    {
        yield return BuildCombination(list, length, i);
    }
}
private static IEnumerable<T> BuildCombination<T>(
    IList<T> list, 
    int length, 
    long combinationNumber)
{
    var temp = combinationNumber;
    for(int j = 0; j < length; j++)
    {
        yield return list[(int)(temp % list.Count)];
        temp /= list.Count;
    }
}

答案 1 :(得分:0)

我认为你已经拥有了解决方案。我对你的代码进行了一些小修改

public static IEnumerable<List<T>> GetCombinations<T>(IEnumerable<T> pool, int comboLength, 
bool isWithReplacement) // changed this to return an enumerable
{
     foreach (var list in GetCombinations(pool, comboLength).Select(c => c.ToList()))
     {
             yield return list; // added a yield return of the list instead of returning a to list of the results
     }
}

private static IEnumerable<IEnumerable<T>> GetCombinations<T>(IEnumerable<T> list, int length)
{
    if (length == 1) return list.Select(t => new[] { t });

    return GetCombinations(list, length - 1).SelectMany(t => list, (t1, t2) => t1.Concat(new[] { t2 }));
}

我测试了这个:

    List<int> items = new List<int>();   

    for (int i = 1; i < 100; i++)
    {
        items.Add(i);
    }
    Stopwatch s = new Stopwatch();
    s.Start();
    int count = 0;
    foreach (var list in GetCombinations(items, 4))
    {
        count++;
    }
    s.Stop();
    Console.WriteLine(count);
    Console.WriteLine(s.ElapsedMilliseconds);
    Console.ReadLine();

这很好,在7587毫秒内没有内存问题,并生成了96,059,601个组合。