对于启发式算法,我需要一个接一个地评估某个集合的组合,直到达到停止标准。
由于它们很多,目前我正在使用以下内存高效迭代器块(受 python 的启发itertools.combinations
)生成它们:
public static IEnumerable<T[]> GetCombinations<T>(this IList<T> pool, int r)
{
int n = pool.Count;
if (r > n)
throw new ArgumentException("r cannot be greater than pool size");
int[] indices = Enumerable.Range(0, r).ToArray();
yield return indices.Select(idx => pool[idx]).ToArray();
while (true)
{
int i;
for (i = r - 1; i >= 0; i--)
if (indices[i] != i + n - r)
break;
if (i < 0)
break;
indices[i] += 1;
for (int j = i + 1; j < r; j++)
indices[j] = indices[j - 1] + 1;
yield return indices.Select(idx => pool[idx]).ToArray();
}
}
问题是,为了大大提高启发式算法的效率,我需要生成这些组合,这些组合按它们的索引之和排序(换句话说,我需要首先生成包含集合中第一个元素的组合)。
例如
考虑集合S = {0,1,2,3,4,5}
(我选择这个集合是为了简单,因为元素和它们的索引是一致的)。从给定算法生成的
所有可能的数字组合是:r=4
(0, 1, 2, 3) SUM: 6
(0, 1, 2, 4) SUM: 7
(0, 1, 2, 5) SUM: 8
(0, 1, 3, 4) SUM: 8
(0, 1, 3, 5) SUM: 9
(0, 1, 4, 5) SUM: 10
(0, 2, 3, 4) SUM: 9
(0, 2, 3, 5) SUM: 10
(0, 2, 4, 5) SUM: 11
(0, 3, 4, 5) SUM: 12
(1, 2, 3, 4) SUM: 10
(1, 2, 3, 5) SUM: 11
(1, 2, 4, 5) SUM: 12
(1, 3, 4, 5) SUM: 13
(2, 3, 4, 5) SUM: 14
如您所见,其中的组合并未严格按升和排序。
相反,期望的结果如下:(
具有相同总和的组合的顺序并不重要)
(0, 1, 2, 3) SUM: 6
(0, 1, 2, 4) SUM: 7
(0, 1, 2, 5) SUM: 8
(0, 1, 3, 4) SUM: 8
(0, 1, 3, 5) SUM: 9
(0, 2, 3, 4) SUM: 9
(0, 1, 4, 5) SUM: 10
(0, 2, 3, 5) SUM: 10
(1, 2, 3, 4) SUM: 10
(0, 2, 4, 5) SUM: 11
(1, 2, 3, 5) SUM: 11
(0, 3, 4, 5) SUM: 12
(1, 2, 4, 5) SUM: 12
(1, 3, 4, 5) SUM: 13
(2, 3, 4, 5) SUM: 14
一个简单的解决方案是生成所有组合,然后根据它们的总和对它们进行排序;但这并不是真正有效/可行的,因为组合的数量随着n
增长而变得巨大。
我还快速浏览了组合格雷码,但找不到适合这个问题的人。
你对如何实现这样的事情有想法吗?
编辑 :
这个问题有一个替代(不幸的是不是更容易)的公式。
给定一个集合S
和一个数字r
,所有可能的和都很容易找到,因为它们只是从 的第一个r
元素之S
和到 的最后一个r
元素之和的所有数字S
。
话虽如此,如果对于每个总和,T
我们可以有效地¹找到所有具有总和的组合,T
我们就可以解决原始问题,因为我们只是按升序生成它们。
¹ 有效意味着我不想生成所有组合并丢弃总和不同的组合。
编辑2:
在@EricLippert 建议后,我创建了以下代码:
public static IEnumerable<T[]>
GetCombinationsSortedByIndexSum<T>(this IList<T> pool, int r)
{
int n = pool.Count;
if (r > n)
throw new ArgumentException("r cannot be greater than pool size");
int minSum = ((r - 1) * r) / 2;
int maxSum = (n * (n + 1)) / 2 - ((n - r - 1) * (n - r)) / 2;
for (int sum = minSum; sum <= maxSum; sum++)
{
foreach (var indexes in AllMonotIncrSubseqOfLenMWhichSumToN(0, n - 1, r, sum))
yield return indexes.Select(x => pool[x]).ToArray();
}
}
static IEnumerable<IEnumerable<int>>
AllMonotIncrSubseqOfLenMWhichSumToN(int seqFirstElement, int seqLastElement, int m, int n)
{
for (int i = seqFirstElement; i <= seqLastElement - m + 1; i++)
{
if (m == 1)
{
if (i == n)
yield return new int[] { i };
}
else
{
foreach (var el in AllMonotIncrSubseqOfLenMWhichSumToN(i + 1, seqLastElement, m - 1, n - i))
yield return new int[] { i }.Concat(el);
}
}
}
这很好用(希望是 Eric 的意思:P),但我仍然担心递归方法的复杂性。事实上,我们似乎正在为每个总和重新生成所有组合,而丢弃那些总和未达到所需值的组合。
为了降低内部函数的复杂性,我找到了一种通过使用有效上限和下限来限制迭代的方法(现在真的很难说这有什么复杂性)。
检查我的答案以查看最终代码。