given a set of n integers, return all subsets of k elements that sum to 0
Asked Answered
C

3

5

given a unsorted set of n integers, return all subsets of size k (i.e. each set has k unique elements) that sum to 0.

So I gave the interviewer the following solution ( which I studied on GeekViewpoint). No extra space used, everything is done in place, etc. But of course the cost is a high time complexity of O(n^k) where k=tuple in the solution.

public void zeroSumTripplets(int[] A, int tuple, int sum) {
  int[] index = new int[tuple];
  for (int i = 0; i < tuple; i++)
    index[i] = i;
  int total = combinationSize(A.length, tuple);
  for (int i = 0; i < total; i++) {
    if (0 != i)
      nextCombination(index, A.length, tuple);
    printMatch(A, Arrays.copyOf(index, tuple), sum);
  }// for
}// zeroSumTripplets(int[], int, int)

private void printMatch(int[] A, int[] ndx, int sum) {
  int calc = 0;
  for (int i = 0; i < ndx.length; i++)
    calc += A[ndx[i]];
  if (calc == sum) {
    Integer[] t = new Integer[ndx.length];
    for (int i = 0; i < ndx.length; i++)
      t[i] = A[ndx[i]];
    System.out.println(Arrays.toString(t));
  }// if
}// printMatch(int[], int[], int)

But then she imposed the following requirements:

  • must use hashmap in answer so to reduce time complexity
  • Must absolutely -- ABSOLUTELY -- provide time complexity for general case
  • hint when k=6, O(n^3)

She was more interested in time-complexity more than anything else.

Does anyone know a solution that would satisfy the new constraints?


EDIT:

Supposedly, in the correct solution, the map is to store the elements of the input and the map is then to be used as a look up table just as in the case for k=2.

When the size of the subset is 2 (i.e. k=2), the answer is trivial: loop through and load all the elements into a map. Then loop through the inputs again this time searching the map for sum - input[i] where i is the index from 0 to n-1, which would then be the answers. Supposedly this trivial case can be extended to where k is anything.

Cyclone answered 3/5, 2012 at 0:26 Comment(13)
There is no question here. Interesting as it is, there is no question. Are you looking for a solution taking into account the added constraints?Eudo
Q: And your question is?Od
edit to add last line: Does anyone know a solution that would satisfy the new constraints?Cyclone
Q: And you're saying that you actually got an interview question you happened to study on GeekViewpoint? And you were able to give the interviewer 20+ lines for working code? That's cool! And she marked you off because of time complexity?!? Isn't that a bit like if your dog responded to a judge's question ... in French ... but the judge marked her down because your dog didn't conjugate her verbs correctly?!?Od
I once got dinged in an interview because I couldn't solve Tangrams quick enough.Tunnell
@Od You are far too easily impressed. (IMO)Dahliadahlstrom
@paulsm4, I wish it was you who interviewed me, then. It's for a senior software position with Google+. It's irresponsible to presume she had a thing against me. She does not know me.Cyclone
@paulsm4, are you saying you don't study cool algorithms you find on the internet? Why are you on stackoverflow?Cyclone
Isn't this the subset-sum problem?Cannell
@Cyclone - I confess. I stumbled on SO by accident. I was actually on the Internet looking for that video of Kate Upton dancing the Dougie ;) Al Gore recommended it ;)Od
@Od You sir are epic :) We need more people here with a sense of humor like you.Normalize
@Cannell Well. There are some differences. The problem you linked is about determining whether there exists a subset which sums to 0. In this problem, all subsets which sum to 0 must be found. Another difference is that this problem is constrained to subsets of one specific size k, as opposed to arbitrary subset size. To me, it feels like they're different problems. I do however agree that this problem can impossibly have a polynomial time solution. Because if a polynomial time solution existed for this problem, then this problem could be used to solve the subset-sum problem in polynomial time.Trackandfield
@Cannell Don't put to much weight to what I said, though. I have no formal education in complexity theory. My only knowledge consists of what I've been reading here on stack overflow and wikipedia.Trackandfield
C
4

Since no-one else has made an attempt, I might as well throw in at least a partial solution. As I pointed out in an earlier comment, this problem is a variant of the subset sum problem and I have relied heavily on documented approaches to that problem in developing this solution.

We're trying to write a function subsetsWithSum(A, k, s) that computes all the k-length subsets of A that sum to s. This problem lends itself to a recursive solution in two ways:

  1. The solution of subsetsWithSum(x1 ... xn, k, s) can be found by computing subsetsWithSum(x2 ... xn, k, s) and adding all the valid subsets (if any) that include x1; and
  2. All the valid subsets that include element xi can be found by computing subsetsWithSum(A - xi, k-1, s-xi) and adding xi to each subset (if any) that results.

The base case for the recursion occurs when k is 1, in which case the solution to subsetsWithSum(A, 1, s) is the set of all single element subsets where that element is equal to s.

So a first stab at a solution would be

/**
 * Return all k-length subsets of A starting at offset o that sum to s.
 * @param A - an unordered list of integers.
 * @param k - the length of the subsets to find.
 * @param s - the sum of the subsets to find.
 * @param o - the offset in A at which to search.
 * @return A list of k-length subsets of A that sum to s.
 */
public static List<List<Integer>> subsetsWithSum(
        List<Integer> A,
        int k,
        int s,
        int o)
{
    List<List<Integer>> results = new LinkedList<List<Integer>>();

    if (k == 1)
    {
        if (A.get(o) == s)
            results.add(Arrays.asList(o));
    }
    else
    {
        for (List<Integer> sub : subsetsWithSum(A, k-1, s-A.get(o), o+1))
        {
            List<Integer> newSub = new LinkedList<Integer>(sub);
            newSub.add(0, o);
            results.add(0, newSub);
        }
    }

    if (o < A.size() - k)
        results.addAll(subsetsWithSum(A, k, s, o+1));

    return results;
}

Now, notice that this solution will often call subsetsWithSum(...) with the same set of arguments that it has been called with before. Hence, subsetsWithSum is just begging to be memoized.

To memoize the function, I've put the arguments k, s and o into a three element list which will be the key to a map from these arguments to a result computed earlier (if there is one):

public static List<List<Integer>> subsetsWithSum(
        List<Integer> A,
        List<Integer> args,
        Map<List<Integer>, List<List<Integer>>> cache)
{
    if (cache.containsKey(args))
        return cache.get(args);

    int k = args.get(0), s = args.get(1), o = args.get(2);
    List<List<Integer>> results = new LinkedList<List<Integer>>();

    if (k == 1)
    {
        if (A.get(o) == s)
            results.add(Arrays.asList(o));
    }
    else
    {
        List<Integer> newArgs = Arrays.asList(k-1, s-A.get(o), o+1);

        for (List<Integer> sub : subsetsWithSum(A, newArgs, cache))
        {
            List<Integer> newSub = new LinkedList<Integer>(sub);
            newSub.add(0, o);
            results.add(0, newSub);
        }
    }

    if (o < A.size() - k)
        results.addAll(subsetsWithSum(A, Arrays.asList(k, s, o+1), cache));

    cache.put(args, results);
    return results;
}

To use the subsetsWithSum function to compute all the k-length subsets that sum to zero, one can use the following function:

public static List<List<Integer>> subsetsWithZeroSum(List<Integer> A, int k)
{
    Map<List<Integer>, List<List<Integer>>> cache =
            new HashMap<List<Integer>, List<List<Integer>>> ();
    return subsetsWithSum(A, Arrays.asList(k, 0, 0), cache);
}

Regrettably my complexity calculating skills are a bit (read: very) rusty, so hopefully someone else can help us compute the time complexity of this solution, but it should be an improvement on the brute-force approach.

Edit: Just for clarity, note that the first solution above should be equivalent in time complexity to a brute-force approach. Memoizing the function should help in many cases, but in the worst case the cache will never contain a useful result and the time complexity will then be the same as the first solution. Note also that the subset-sum problem is NP-complete meaning that any solution has an exponential time complexity. End Edit.

Just for completeness, I tested this with:

public static void main(String[] args) {
    List<Integer> data = Arrays.asList(9, 1, -3, -7, 5, -11);

    for (List<Integer> sub : subsetsWithZeroSum(data, 4))
    {
        for (int i : sub)
        {
            System.out.print(data.get(i));
            System.out.print(" ");
        }

        System.out.println();
    }
}

and it printed:

9 -3 5 -11
9 1 -3 -7
Cannell answered 4/5, 2012 at 4:50 Comment(8)
up vote! I test your functions and they work. I am going to try my hand at the time complexity. Thanks!Cyclone
The time complexity of this recurrence seems to be T(n) = 2T(n-1)+C where n is the input size and C is a constant. Solving with a recurrence tree I get O(2^n). Will someone else please verify this? I think there is a problem with my computation since k does not show up anywhereCyclone
actually, neither of your algorithms is faster than mine.Cyclone
@Cyclone I've edited my answer to clarify a little. I expected the first of my solutions to have a time complexity roughly equivalent to your approach, it was the memoization in the second solution that I expected to help. In the worst case, however, the cache might never contain a useful result and the time complexity is the same as my first solution. That's why I called it a "partial" solution - it satisfies the first of your three requirements (that a HashMap be used), but I couldn't give you the other two.Cannell
Take a look at these other stackoverflow questions tagged with subset-sum for more potential answers to your question. You might consider adding the subset-sum tag to this question too.Cannell
Thanks. I will look at the others.Cyclone
Supposedly, in the correct solution, the map is to store the elements of the input and the map is then to be used as a look up table just as in the case for k=2Cyclone
I got more difficult question: "number of subsets of 3 elements that average to k (divide by 3 and floor)". I solved it brute-force because I had only 20 minutes on it. Perhaps approach 1 is a little faster, but I don't like recursion in JavaBusoni
A
3

I think your answer was very close to what they were looking for, but you can improve the complexity by noticing that any subset of size k can be thought of as two subsets of size k/2. So instead of finding all subsets of size k (which takes O(n^k) assuming k is small), use your code to find all subsets of size k/2, and put each subset in a hashtable, with its sum as the key.

Then iterate through each subset of size k/2 with a positive sum (call the sum S) and check the hashtable for a subset whose sum is -S. If there is one then the combination of the two subsets of size k/2 is a subset of size k whose sum is zero.

So in the case of k=6 that they gave, you would find all subsets of size 3 and compute their sums (this will take O(n^3) time). Then checking the hashtable will take O(1) time for each subset, so the total time is O(n^3). In general this approach will take O(n^(k/2)) assuming k is small, and you can generalize it for odd values of k by taking subsets of size floor(k/2) and floor(k/2)+1.

Armorial answered 22/5, 2012 at 22:48 Comment(1)
I think that in this case you would have problem of possibly using some elements of the array more than once. You'll also need to add some mechanism that will assure you that every element is occurred at maximum once in the resulting array. eg. array = {-1, 2, 3, 4, -3} k=4, and using your approach you will get {2, -3} and {4, -3} as result, but -3 is used twice, same for {-1, 2} and {2, -3} this time 2 is used twice.Waterspout
O
2

@kasavbere -

Recently a friend had one of those harrowing all-day interviews for a C++ programming job with Google. His experience was similar to yours.

It inspired him to write this article - I think you might enjoy it:

The Pragmatic Defense

Od answered 3/5, 2012 at 4:57 Comment(1)
Great article. +1. I like the positive and thus responsible reaction. While my case was just a phone interview which I in no way expected to be so daunting, it is what it is. And if someone here has a technique for quickly computing such non-obvious time complexities, I would appreciate their input.Cyclone

© 2022 - 2024 — McMap. All rights reserved.