给定一组n个整数,返回总和为0的k个元素的所有子集

时间:2012-05-03 00:26:59

标签: java time-complexity subset-sum

给定一组未排序的n整数,返回大小为k的所有子集(即每个集合具有k个唯一元素),总和为0.

所以我给了面试官以下解决方案(我在GeekViewpoint上学习过)。没有使用额外的空间,一切都已就位,等等。但当然,成本是O(n ^ k)的高时间复杂度,其中k=tuple在解决方案中。

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)

但后来她强加了以下要求:

  • 必须在回答中使用hashmap以减少时间复杂度
  • 绝对必须 - 绝对 - 为一般情况提供时间复杂性
  • 当k = 6,O(n ^ 3)
  • 时提示

她对时间复杂性的兴趣比什么都重要。

有没有人知道满足新约束的解决方案?


编辑:

据说,在正确的解决方案中,地图将存储输入的元素,然后地图将用作查找表,就像k=2的情况一样。

当子集的大小为2(即k=2)时,答案是微不足道的:循环并将所有元素加载到地图中。然后再次遍历输入,这次在地图中搜索sum - input[i] where i is the index from 0 to n-1,这将是答案。据说这个简单的案例可以扩展到k是什么。

3 个答案:

答案 0 :(得分:4)

由于没有其他人做过尝试,我不妨投入至少部分解决方案。正如我在之前的评论中指出的那样,这个问题是subset sum problem的一个变体,我在开发这个解决方案时严重依赖于记录的方法来解决这个问题。

我们正在尝试编写一个函数subsetsWithSum(A, k, s)来计算总和为s的所有k长度的子集。这个问题有两个方面的递归解决方案:

  1. 通过计算subsetsWithSum(x 2 ,可以找到subsetsWithSum(x 1 ... x n ,k,s)的解。 ... x n ,k,s)并添加包含x 1 的所有有效子集(如果有);和
  2. 通过计算subsetsWithSum(A - x i ,k-1,sx i i 的所有有效子集>)并将x i 添加到结果的每个子集(如果有的话)。
  3. 当k为1时,会发生递归的基本情况,在这种情况下,subsetsWithSum(A,1,s)的解是所有单个元素子集的集合,其中该元素等于s。

    所以第一次尝试解决方案就是

    /**
     * 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;
    }
    

    现在,请注意,此解决方案通常会使用之前调用的相同参数集调用subsetsWithSum(...)。因此,subsetsWithSum只是乞求memoized

    为了记住这个函数,我把参数k,s和o放到一个三元素列表中,它将是从这些参数到先前计算的结果(如果有的话)的映射的关键:

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

    要使用subsetsWithSum函数计算总和为零的所有k长度子集,可以使用以下函数:

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

    令人遗憾的是,我的复杂性计算技巧有点(阅读:非常)生锈,所以希望其他人可以帮助我们计算这个解决方案的时间复杂度,但它应该是对蛮力方法的改进。

    编辑:为了清楚起见,请注意上面的第一个解决方案应该在时间复杂度上与蛮力方法相当。在许多情况下,记住函数应该有所帮助,但在最坏的情况下,缓存永远不会包含有用的结果,时间复杂度将与第一个解决方案相同。还要注意,子集和问题是NP-complete,这意味着任何解决方案都具有指数时间复杂度。 结束编辑。

    为了完整起见,我用以下方法进行了测试:

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

    并打印出来:

    9 -3 5 -11
    9 1 -3 -7
    

答案 1 :(得分:3)

我认为您的答案非常接近他们所寻找的内容,但您可以通过注意到任何大小k的子集可以被视为两个大小为k/2的子集来提高复杂性。因此,不要找到大小为k的所有子集(假设O(n^k)k需要小),请使用您的代码查找大小为k/2的所有子集,并放置每个子集在哈希表中,以和为键。

然后使用正总和(称为总和k/2)遍历大小S的每个子集,并检查哈希表中的总和为-S的子集。如果有,那么大小为k/2的两个子集的组合是大小为k的子集,其总和为零。

因此,对于他们提供的k=6,您会找到所有大小为3的子集并计算其总和(这将花费O(n^3)次)。然后检查哈希表将为每个子集花费O(1)时间,因此总时间为O(n^3)。一般来说,这种方法将O(n^(k/2))假设k很小,您可以通过获取大小为kfloor(k/2)的子集将其推广为floor(k/2)+1的奇数值

答案 2 :(得分:2)

@kasavbere -

最近,一位朋友对谷歌的C ++编程工作进行了全天采访。他的经历与你的相似。

这激励他写这篇文章 - 我想你可能会喜欢它:

The Pragmatic Defense