【问题标题】:Pick a varying number of item combinations from a List从列表中选择不同数量的项目组合
【发布时间】:2026-01-23 10:15:02
【问题描述】:

假设我有一个任意长度的整数列表,例如我有 1、3、5 和 7 的列表。

我想要一个算法来从列表中选择 X 元素的组合。

例如,X = 1 会返回:

1

3

5

7

x = 2 将返回:

1 + 1

1 + 3

1 + 5

1 + 7

3 + 3

3 + 5

3 + 7

5 + 5

5 + 7

7 + 7

var listOfInts = new List<int> { 1, 3, 5, 7 };
var combinedInts = new List<int>();

// x = 1 solution
// This is only picking one item from the list. 
for (int i = 0; i < listOfInts.Count(); i++)
{
    combinedInts.Add(listOfInts[i]);
}

// x = 2 solution
// This is how to pick two. I wrap it around another for loop.
for (int i = 0; i < listOfInts.Count(); i++)
{
    for (int j = i; j < listOfInts.Count(); j++)
    {
        combinedInts.Add(listOfInts[i] + listOfInts[j]);
    }
}

// x = 3 solution
// If I go up another level I have to wrap it around another for loop. This solution won't scale.
for (int i = 0; i < listOfInts.Count(); i++)
{
    for (int j = i; j < listOfInts.Count(); j++)
    {
        for (int k = j; k < listOfInts.Count(); k++)
        {
            combinedInts.Add(listOfInts[i] + listOfInts[j] + listOfInts[k]);
        }
    }
}

此解决方案无法扩展,因为我必须为我选择的每个元素数量不断地环绕另一个 for 循环。例如 X = 7 需要 7 个嵌套的 for 循环。有没有更好的方法来编写这个不涉及嵌套 for 循环的方法?

【问题讨论】:

  • 您希望递归地调用单个循环的函数。递归示例见dotnetperls.com/recursion
  • 这实际上应该是 *.com/questions/4073713/… 的副本 - 您正在寻找 x 个列表的笛卡尔积。要获得总和,您只需将其汇总即可。
  • @Rob 你是对的 - 我选错了一个(在你发表评论时重新打开:))。请注意,“选择一个不同的数字”可能意味着 combinations of k items from n,但 sample 确实谈到了笛卡尔积。
  • @Layoric,递归的例子,实际见here
  • 我不相信建议的副本回答了这个问题。首先,在问题的示例中,一旦选择了一个元素,只有从该点开始的元素用于组合(即不是真正的笛卡尔积)。其次,OP 专门要求聚合任意多个序列,因此存在一个简单的笛卡尔积没有解决的递归元素(即使这是一个真正的笛卡尔积)。

标签: c# .net for-loop


【解决方案1】:

这对我有用:

Func<IEnumerable<int>, int, IEnumerable<IEnumerable<int>>> generate = null;
generate = (xs, n) =>
    (xs == null || !xs.Any())
        ? Enumerable.Empty<IEnumerable<int>>()
        : n == 1
            ? xs.Select(x => new [] { x })
            : xs.SelectMany(x => generate(xs, n - 1).Select(ys => ys.Concat(new [] { x })));

int[] array = { 1, 3, 5, 7, };

var results =
    generate(array, 3)
        .Select(xs => String.Join("+", xs));

通过这个电话我得到:

1+1+1、3+1+1、5+1+1、7+1+1、1+3+1、3+3+1、5+3+1、7+3+1 , 1+5+1, 3+5+1, 5+5+1, 7+5+1, 1+7+1, 3+7+1, 5+7+1, 7+7+1, 1 +1+3、3+1+3、5+1+3、7+1+3、1+3+3、3+3+3、5+3+3、7+3+3、1+5 +3、3+5+3、5+5+3、7+5+3、1+7+3、3+7+3、5+7+3、7+7+3、1+1+5 , 3+1+5, 5+1+5, 7+1+5, 1+3+5, 3+3+5, 5+3+5, 7+3+5, 1+5+5, 3 +5+5、5+5+5、7+5+5、1+7+5、3+7+5、5+7+5、7+7+5、1+1+7、3+1 +7、5+1+7、7+1+7、1+3+7、3+3+7、5+3+7、7+3+7、1+5+7、3+5+7 , 5+5+7, 7+5+7, 1+7+7, 3+7+7, 5+7+7,7+7+7

【讨论】:

    【解决方案2】:

    您可以使用以下方法获取序列的组合

    public static class LinqHelper
    {
        public static IEnumerable<IEnumerable<T>> Combinations<T>(this IEnumerable<T> elements, int? k = null)
        {
            if (!k.HasValue)
                k = elements.Count();
    
            return k == 0 ? new[] { new T[0] } :
               elements.SelectMany((e, i) => elements.Skip(i).Combinations(k - 1).Select(c => (new[] { e }).Concat(c)));
        }
    }
    
    var list = new List<int> { 1, 3, 5, 7 };
    
    int x = 2; //Change to 3, 4, 5, etc
    
    var result = list.Combinations(x);
    

    产量:

    1 1
    1 3
    1 5
    1 7
    3 3
    3 5
    3 7
    5 7
    7 7

    要获得每个的总和,您需要汇总结果:

    var result = list.Combinations(x).Select(g => g.Aggregate((left, right) => left + right));
    

    产生:

    2
    4
    6
    8
    6
    8
    10
    10
    12
    14

    【讨论】:

    • 感谢您澄清我的问题以及如何解决它。我正在更新问题,以更好地反映我最初提出的问题。
    【解决方案3】:

    还有一种纯粹的迭代方式可以做到这一点。它需要更多的思考和复杂性,但可以变得非常有效。基本思想是模拟相同的嵌套循环,但将每个嵌套循环的迭代跟踪为一个 循环计数器 数组,它们以与原始嵌套循环代码相同的方式向前迭代。这是一个完整的示例:

    var listOfInts = new List<int> { 1, 3, 5, 7 };
    var combinedInts = new List<int>();
    
    var numInts = listOfInts.Count;
    var numElements = 5; // number of "nested loops", or ints selected in each combination
    var loopCounters = new int[numElements]; // make one loop counter for each "nested loop"
    var lastCounter = numElements - 1; // iterate the right-most counter by default
    
    // maintain current sum in a variable for efficiency, since most of the time
    // it is changing only by the value of one loop counter change.
    var tempSum = listOfInts[0] * numElements;
    
    // we are finished when the left/outer-most counter has looped past number of ints
    while (loopCounters[0] < numInts) {
        // you can use this to verify the output is iterating correctly:
        // Console.WriteLine(string.Join(",", loopCounters.Select(x => listOfInts[x])) + ": " + loopCounters.Select(x => listOfInts[x]).Sum() + "; " + tempSum);
    
        combinedInts.Add(tempSum);
    
        tempSum -= listOfInts[loopCounters[lastCounter]];
        loopCounters[lastCounter]++;
        if (loopCounters[lastCounter] < numInts) tempSum += listOfInts[loopCounters[lastCounter]];
    
        // if last element reached in inner-most counter, increment previous counter(s).
        while (lastCounter > 0 && loopCounters[lastCounter] == numInts) {
            lastCounter--;
            tempSum -= listOfInts[loopCounters[lastCounter]];
            loopCounters[lastCounter]++;
            if (loopCounters[lastCounter] < numInts) tempSum += listOfInts[loopCounters[lastCounter]];
        }
    
        // if a previous counter was advanced, reset all future counters to same
        // starting number to start iteration forward again.
        while (lastCounter < numElements - 1) {
            lastCounter++;
            if (loopCounters[lastCounter] < numInts) tempSum -= listOfInts[loopCounters[lastCounter]];
            loopCounters[lastCounter] = loopCounters[lastCounter - 1];
            if (loopCounters[lastCounter] < numInts) tempSum += listOfInts[loopCounters[lastCounter]];
        }
    }
    

    在迭代结束时,combinedInts 应该包含所有和组合的列表,类似于原始代码或其他递归解决方案。如果您正在使用小集合和小组合,那么这种效率水平是不必要的,您应该更喜欢更容易推理正确性的递归解决方案。我将此作为思考问题的另一种方式。干杯!

    【讨论】:

    • 我肯定会检查一下,因为我对这种方法的实际需要是确定是否可以通过选择列表中的 X 元素来创建特定数字。例如,我可以通过从 1、3、5、7 中选择 4 个项目来创建数字 14 吗? (是的,1,3,5,5)。这种迭代解决方案似乎更适合这项任务。
    • @wentimo:请注意,一旦找到答案,其他海报提出的递归/生成解决方案也可以通过短路来同样适用。例如,在 LINQ 生成器解决方案中使用 .First(x =&gt; x == 14) 而不是 .Select(),或者将所需的总和传递给递归并在找到后立即返回 true。如果您正在寻找原始速度,我认为我的解决方案将扩展得最好,但代价是代码更难以理解和维护。