3

Assume I have a list of integers of any length, for an example I have the list of 1,3,5 and 7.

I would like an algorithm to pick a combination of X elements from the list.

For example, X = 1 would return:

1

3

5

7

x = 2 would return:

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]);
        }
    }
}

This solution doesn't scale as I have to continually wrap around another for loop for each number of element I'm picking. For example X = 7 would need 7-nested for loops. Is there a better way to write this method that doesn't involve nesting for loops?

wentimo
  • 471
  • 3
  • 12
  • You're looking to call a function of the single loop *recursively*. For example of recursion, see http://www.dotnetperls.com/recursion – Darren Reid Mar 21 '16 at 03:08
  • 1
    This should actually be a duplicate of http://stackoverflow.com/questions/4073713/is-there-a-good-linq-way-to-do-a-cartesian-product - you're looking for the Cartesian product of x-number of lists. To get the sum, you'd simple aggregate it. – Rob Mar 21 '16 at 03:16
  • 1
    @Rob you are right - I picked wrong one (reopened while you've commented :) ). Note that "pick a varying number" may mean [combinations of k items from n](http://stackoverflow.com/questions/127704/algorithm-to-return-all-combinations-of-k-elements-from-n), but sample indeed talks about Cartesian product. – Alexei Levenkov Mar 21 '16 at 03:18
  • @Layoric, for an example of recursion, actually see [here](http://stackoverflow.com/questions/36122487/pick-a-varying-number-of-items-from-a-list-without-nesting-for-loops) – Jonesopolis Mar 21 '16 at 03:34
  • 3
    I'm not convinced the proposed duplicate answers the question. First, in the question's example, once an element has been selected, only the elements from that point on are used for the combination (i.e. not really Cartesian product). Second, the OP is specifically asking to aggregate arbitrarily many sequences, so there's a recursive element not addressed by a simple Cartesian product (even if this were a true Cartesian product). – Peter Duniho Mar 21 '16 at 03:41
  • @PeterDuniho You're absolutely right - my bad. – Rob Mar 21 '16 at 03:47

3 Answers3

3

You can use the following to get combinations of the sequences:

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);

Yields:

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

To get the sum of each one, you'd aggregate the result:

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

Which produces:

2
4
6
8
6
8
10
10
12
14

Rob
  • 26,989
  • 16
  • 82
  • 98
  • thank you for both clarification on what I was asking and how to solve it. I'm updating the question to better reflect what I was asking in the first place. – wentimo Mar 21 '16 at 04:05
2

There is also a purely iterative way to do this. It requires a great deal more thought and complexity, but can be made very efficient. The basic idea is to simulate the same nested loops, but track the iterations of each nested loop as an array of loop counters, which are iterated forward in the same manner as the original nested loop code. Here is a fully working example:

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

At the end of the iteration, combinedInts should contains a list of all sum combinations, similar to the original code or the other recursive solutions. If you are working with small sets, and small combinations, then this level of efficiency is unnecessary and you should prefer a recursive solution which is easier to reason about correctness. I present this as an alternative way to think about the problem. Cheers!

mellamokb
  • 56,094
  • 12
  • 110
  • 136
  • I'm definitely going to check this out as the actual need I have for this method is to determine if a specific number can be created by selecting X elements in the list. So for example, can I create the number 14 by selecting 4 items from 1,3,5,7? (Yes, 1,3,5,5). This iterative solution seems to be better suited for this task. – wentimo Mar 21 '16 at 17:42
  • @wentimo: Note that the recursive/generating solutions presented by the other posters can also be made equally suited by short-circuiting as soon as an answer is found. For example using `.First(x => x == 14)` instead of `.Select()` in the LINQ generator solution, or passing a desired sum into the recursion and returning `true` as soon as it is found. If you're looking for raw speed, I think my solution will scale the best, but at the cost of code that is more difficult to understand and maintain. – mellamokb Mar 21 '16 at 20:05
1

This works for me:

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));

With this call I get:

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

Enigmativity
  • 113,464
  • 11
  • 89
  • 172