5

给定一组未排序的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,这就是答案。据说这个微不足道的案例可以扩展到 where kis anything。

4

3 回答 3

4

由于没有其他人尝试过,我不妨至少提供一个部分解决方案。正如我在之前的评论中指出的那样,这个问题是子集和问题的一个变体,我在开发这个解决方案时严重依赖于该问题的文档化方法。

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

  1. subsetsWithSum(x 1 ... x n , k, s) 的解可以通过计算 subsetsWithSum(x 2 ... x n , k, s) 并添加包括 x 1的所有有效子集(如果有)来找到; 和
  2. 可以通过计算 subsetsWithSum(A - x i , k-1, sx i ) 并将 x i添加到结果的每个子集(如果有)来找到包含元素 x i的所有有效子集。

递归的基本情况发生在 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只是乞求被记忆。

为了记住这个函数,我将参数 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 完全的,这意味着任何解决方案都具有指数时间复杂度。结束编辑。

为了完整起见,我对此进行了测试:

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
于 2012-05-04T04:50:24.340 回答
3

我认为您的答案非常接近他们正在寻找的内容,但是您可以通过注意到 size 的任何子集都k可以被认为是 size 的两个子集来提高复杂性k/2。因此,不要查找所有 size 的子集kO(n^k)假设k它很小),而是使用您的代码查找 size 的所有子集k/2,并将每个子集放入哈希表中,并以其总和作为键。

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

所以在k=6他们给出的情况下,你会找到所有大小的子集3并计算它们的总和(这需要O(n^3)时间)。然后检查哈希表将花费O(1)每个子集的时间,因此总时间为O(n^3). 一般来说,这种方法会O(n^(k/2))假设k很小,并且您可以k通过采用大小floor(k/2)和的子集将其推广到奇数值floor(k/2)+1

于 2012-05-22T22:48:35.467 回答
2

@kasavbere -

最近,一位朋友在谷歌接受了一个 C++ 编程工作的令人痛苦的全天面试。他的经历与你相似。

它启发了他写这篇文章 - 我想你可能会喜欢它:

务实的防御

于 2012-05-03T04:57:49.620 回答