此处的目标是使用递归生成所有与替换的组合,使其不超过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算法的人是理想的。
答案 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个组合。