2

I have a pool of options in groups and I'm trying to dynamically generate the combinations for testing purposes. I would like to define the buckets and have code generating all the combinations to be fed to my TestNG test via @DataProvider. Right now I have some cases hardcoded but it's obvious is not the best way of doing it for maintaining the code.

I'm struggling to handle the case where you have x "balls" in y "buckets" when y is > 2.

In the trivial case let's say you have the following example:

public static void main(String [] args){
   Object[][] combinations = getCombinations(
        new String[]
        {
          "1", "2"
        },
        new String[]
        {
          "3", "4"
        }/*,
        new String[]
        {
          "5", "6"
        }*/);
   for (Object[] combination : combinations)
   {
     System.out.println(Arrays.toString(combination));
   }
}

private Object[][] getCombinations(Object[]... arrays)
{
   if (arrays.length == 0)
   {
     return new Object[0][0];
   }

   List<Object[]> solutions = new ArrayList<>();
   Object[] array1 = arrays[0];
   for (Object o : array1)
   {
     for (int i = 1; i < arrays.length; i++)
     {
       for (Object o2 : arrays[i])
       {
         int count = 0;
         Object[] path = new Object[arrays.length];
         path[count++] = o;
         path[count++] = o2;
         solutions.add(path);
       }
     }
   }
return solutions.toArray(new Object[0][0]);
}

Output:

[1, 3]
[1, 4]
[2, 3]
[2, 4]

Adding the third "bucket" throws everything out the window.

The solutions would be as follows:

[1,3,5]
[1,3,6]
[1,4,5]
[1,4,6]
[2,3,5]
[2,3,6]
[2,4,5]
[2,4,6]

Any ideas how to attack this issue? Ideally you would pass getCombinations the amount of picks per bucket.

Although a solution code would be welcomed, I'm more interested in the reasoning behind it.

Update For future visitors here's the great answer by Kevin Anderson in a generic form:

Unit Test:

import static org.testng.Assert.assertEquals;

import java.util.Arrays;
import java.util.List;

import org.testng.annotations.Test;

public class CombinationNGTest
{
  @Test
  public void testCombinaitonOnePick()
  {
    List<List<Integer>> result
            = Combination.pickKfromEach((List<List<Integer>>) Arrays.asList(
                    Arrays.asList(1, 2),
                    Arrays.asList(3, 4)),
                    1);

    assertEquals(result.size(), 4, result.toString());

    result = Combination.pickKfromEach((List<List<Integer>>) Arrays.asList(
            Arrays.asList(1, 2),
            Arrays.asList(3, 4),
            Arrays.asList(5, 6)),
            1);

    assertEquals(result.size(), 8, result.toString());

    result = Combination.pickKfromEach((List<List<Integer>>) Arrays.asList(
            Arrays.asList(1, 2),
            Arrays.asList(3, 4),
            Arrays.asList(5, 6),
            Arrays.asList(7, 8)),
            1);

    assertEquals(result.size(), 16, result.toString());

    List<List<String>> result2= Combination.pickKfromEach((List<List<String>>) Arrays.asList(
                    Arrays.asList("A", "B"),
                    Arrays.asList("C", "D")),
                    1);

    assertEquals(result2.size(), 4, result.toString());
  }

  @Test
  public void testCombinaitonMultiplePicks()
  {
    List<List<Integer>> result
            = Combination.pickKfromEach((List<List<Integer>>) Arrays.asList(
                    Arrays.asList(1, 2, 3),
                    Arrays.asList(4, 5, 6)),
                    2);

    assertEquals(result.size(), 9, result.toString());
  }
}
4
  • If we add the third bucket, is the solution [1,3], [1,4], [1,5], [1,6], [2,3]. [2,4], [2,5], [2,6] or [1,3,5], [1,3,6], [1,4,5], [1,4,6], [2,3,5], [2,3,6], [2,4,5], [2,4,6]? Commented Oct 10, 2018 at 22:08
  • [1,3,5], [1,3,6], [1,4,5], [1,4,6], [2,3,5], [2,3,6], [2,4,5], [2,4,6] Commented Oct 10, 2018 at 22:09
  • You forgot to say what you're trying to do. Commented Oct 10, 2018 at 22:22
  • Added some background to the reason behind the question, but not sure what was missing. Commented Oct 10, 2018 at 22:30

2 Answers 2

1

You've hit on an overly complicated solution which, nonetheless, just happens to work for the case of two buckets. However, as you have discovered, it won't extend naturally to three or more buckets.

Here's a simpler solution for the two-bucket case, generified and using Lists in place of arrays:

// Find all 2-item combinations consisting of 1 item picked from 
// each of 2 buckets
static <T> List<List<T>> pick1From2(List<List<T>> in)
{
    List<List<T>> result = new ArrayList<>();
    for (int i = 0; i < in.get(0).size(); ++i) {
        for (int j = 0; j < in.get(1).size(); ++j) {
            result.add(Arrays.asList(in.get(0).get(i), in.get(1).get(j)));
        }
    }
    return result;
}

The outer loop runs over all the elements of the first bucket and for each element of the first bucket, the inner loop runs over the elements of the second bucket.

For three buckets, you can just add a third level of loop nesting:

// Find all 3-item combinations consisting of 1 item picked from
// each of 3 buckets 
static <T> List<List<T>> pick1From3(List<List<T>> in)
{
    List<List<T>> result = new ArrayList<>();
    for (int i = 0; i < in.get(0).size(); ++i) {
        for (int j = 0; j < in.get(1).size(); ++j) {
            for (int k = 0; k < in.get(2).size(); ++k)
                result.add(Arrays.asList(in.get(0).get(i), in.get(1).get(j), in.get(2).get(k)));
        }
    }
    return result;
}

Now you have the outer loop stepping through the items of the first bucket, an intermediate loop stepping through the items of the second bucket, and an innermost loop stepping over the elements of the third bucket.

But this approach is limited by the fact that the depth of loop nesting needed is directly related to the number of buckets to be processed: Sure, you can add a fourth, a fifth, etc., level of loop nesting to handle four, five, or more buckets. However, the basic problem remains: you have to keep modifying the code to accommodate ever-increasing numbers of buckets.

The solution to the dilemma is a single algorithm which accommodate any number, N, of buckets by effectively simulating for loops nested to N levels. An array of N indices will take the place of the N loop control variables of N nested for statements:

// Find all `N`-item combinations consisting 1 item picked from 
// each of an `N` buckets
static <T> List<List<T>> pick1fromN(List<List<T>> s)
{
    List<List<T>> result = new ArrayList<>();
    int[] idx = new int[s.size()];
    while (idx[0] < s.get(0).size()) {
        List<T> pick = new ArrayList(s.size());
        for (int i = 0; i < idx.length; ++i) {
            pick.add(s.get(i).get(idx[i]));
        }
        result.add(pick);
        int i = idx.length - 1;
        while (++idx[i] >= s.get(i).size() && i > 0) {
            idx[i] = 0;
            --i;
        }
    }
    return result;
}

The indices all start off at zero, and each maxxes out upon reaching the size of the corresponding bucket. To step to the next combination (inner while loop) the last index index is incremented; if it has maxxed out, it is reset to zero and the next higher index is incremented. If the next higher index also maxes out, it resets and causes the next index to increment, and so on. idx[0] never resets after it increments, so that the outer while can detect when idx[0] has maxxed out.

Picking k items from each bucket is basically the same process, except with the sets of k-combinations of the buckets substituted for the original buckets:

// Find all `N * k`-item combinations formed by picking `k` items
// from each of `N` buckets
static <T> List<List<T>> pickKfromEach(List<List<T>> sets, int k)
{
    List<List<List<T>>> kCombos = new ArrayList<>(sets.size());
    for (List<T> ms : sets) {
        kCombos.add(combinations(ms, k));
    }
    ArrayList<List<T>> result = new ArrayList<>();
    int[] indices = new int[kCombos.size()];
    while (indices[0] < kCombos.get(0).size()) {
        List<T> pick = new ArrayList<>(kCombos.size());
        for (int i = 0; i < indices.length; ++i) {
            pick.addAll(kCombos.get(i).get(indices[i]));
        }
        result.add(pick);
        int i = indices.length - 1;
        while (++indices[i] >= kCombos.get(i).size() && i > 0) {
            indices[i] = 0;
            --i;
        }
    }
    return result;
}

static <T> List<List<T>> combinations(List<T> s, int k) throws IllegalArgumentException
{
    if (k < 0 || k > s.size()) {
        throw new IllegalArgumentException("Can't pick " + k
            + " from set of size " + s.size());
    }
    List<List<T>> res = new LinkedList<>();
    if (k > 0) {
        int idx[] = new int[k];
        for (int ix = 0; ix < idx.length; ++ix) {
            idx[ix] = ix;
        }
        while (idx[0] <= s.size() - k) {
            List<T> combo = new ArrayList<>(k);
            for (int ix = 0; ix < idx.length; ++ix) {
                combo.add(s.get(idx[ix]));
            }
            res.add(combo);
            int ix = idx.length - 1;
            while (ix > 0 && (idx[ix] == s.size() - k + ix))
               --ix;
            ++idx[ix];
            while (++ix < idx.length)
                idx[ix] = idx[ix-1]+1;
        }
    }
    return res;
}

Like the pick routine, the combinations method uses an array of indices to enumerate the combinations. But the indices are managed a bit differently. The indices start out at {0, 1, 2, ..., k-1_}, and they max-out when they have reached the values {n - k, n - k + 1, ..., n}. To step to the next combination, last index which has not yet maxed-out is incremented, and then each following index is reset to the value of the one above it, plus one.

Sign up to request clarification or add additional context in comments.

5 Comments

Although this solves part of the question as it doesn't handle picking x amount of items per bucked I do realize that such requirement might make it impossible to do.
If you feel that's impossible to do I can mark yours as answer and update question accordingly.
Well I noticed the question might be misleading specifying one per bucket but the description says otherwise.
Picking more than one from each bucket: not impossible, just a little more work. Instead of iterating through the individual items in each bucket, we have to form all the combinations of that many items and iterate over the combinations. I'll get back to you...
@javydreamercsw Updated with code for multiple selections per bucket.
1

The Problem you are struggling with can not easily be solved iteratively, since the complexity changes with the amount of given Arrays. A solution to this problem is the use of a recursive function that generates the Permutations of the first Argument and all the following Arrays.

Unfortunately i can't write any fully working code right now, but i can try to give you an example:

public static Object[] permuteAll(Object[] objs1, Object[][] objs2) {
    if(objs2.length == 1){
        return permuteAll(objs1, objs2);
    }else{
        return permuteAll(objs2[0], objs2[/*The rest of the objs[][]*/]]);
    }
}

public static Object[] permuteAll(Object[] objs1, Object[] objs2) {
    return ... //Your Code for 2 buckets goes here
}

I would also recommend using Generics instead of the Object class, but depending on the way you combine your objects you might not get any real benefit out of this...

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.