【问题标题】:given a set of n integers, return all subsets of k elements that sum to 0给定一组 n 个整数,返回总和为 0 的 k 个元素的所有子集
【发布时间】:2012-05-03 00:26:59
【问题描述】:

给定一组未排序的 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 是什么。

【问题讨论】:

  • 这里没有问题。尽管很有趣,但毫无疑问。您是否正在寻找一个考虑到附加约束的解决方案?
  • 问:你的问题是?
  • 编辑添加最后一行:Does anyone know a solution that would satisfy the new constraints?
  • 问:你是说你实际上得到了一个你碰巧在 GeekViewpoint 上研究的面试问题?你能给面试官 20 多行工作代码吗?这很酷!由于时间复杂度,她将你标记为关闭?!?这不是有点像如果你的狗回答了法官的问题......用法语......但是法官因为你的狗没有正确地结合她的动词而给她打分?!?
  • @kasavbere - 我承认。我偶然发现了SO。我实际上是在互联网上寻找凯特厄普顿跳舞 Dougie 的视频;)阿尔·戈尔推荐它;)

标签: java time-complexity subset-sum


【解决方案1】:

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

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

  1. subsetsWithSum(x1 ... xn, k, s)的解可以通过计算subsetsWithSum(x2得到... xn, k, s) 并添加包含 x1 的所有有效子集(如果有);和
  2. 可以通过计算 subsetsWithSum(A - xi, k-1, sxi 找到所有包含元素 xi 的有效子集>) 并将 xi 添加到结果的每个子集(如果有)。

递归的基本情况发生在 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

【讨论】:

  • 赞成!我测试了你的功能并且它们有效。我将尝试时间复杂度。谢谢!
  • 这次复现的时间复杂度好像是T(n) = 2T(n-1)+C where n is the input size and C is a constant。用递归树求解我得到O(2^n)。请其他人验证这一点吗?我认为我的计算存在问题,因为 k 没有出现在任何地方
  • 其实,你们的算法都没有我的快。
  • @kasavbere 我已经编辑了我的答案以澄清一点。我希望我的第一个解决方案的时间复杂度与您的方法大致相当,我希望第二个解决方案中的记忆能够提供帮助。然而,在最坏的情况下,缓存可能永远不会包含有用的结果,并且时间复杂度与我的第一个解决方案相同。这就是为什么我称它为“部分”解决方案 - 它满足您的三个要求中的第一个(使用 HashMap),但我不能给您另外两个。
  • 查看这些other stackoverflow questions tagged with subset-sum 以获得更多可能的问题答案。您也可以考虑在这个问题中添加子集和标签。
【解决方案2】:

我认为您的答案与他们所寻找的非常接近,但是您可以通过注意任何大小为 k 的子集都可以被视为大小为 k/2 的两个子集来提高复杂性。因此,不要查找大小为k 的所有子集(假设k 很小,则需要O(n^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 很小,您可以通过采用大小为 floor(k/2)floor(k/2)+1 的子集将其推广到 k 的奇数值。

【讨论】:

  • 我认为在这种情况下,您可能会遇到多次使用数组的某些元素的问题。您还需要添加一些机制,以确保每个元素在结果数组中最多出现一次。例如。 array = {-1, 2, 3, 4, -3} k=4,使用你的方法你会得到 {2, -3} 和 {4, -3} 作为结果,但是 -3 被使用了两次,相同对于 {-1, 2} 和​​ {2, -3},这次 2 使用了两次。
【解决方案3】:

@kasavbere -

最近,一位朋友参加了 Google 的 C++ 编程工作一整天令人痛苦的面试。他的经历与你相似。

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

The Pragmatic Defense

【讨论】:

  • 很棒的文章。 +1。我喜欢积极而负责任的反应。虽然我的案子只是一次电话采访,我没想到会如此令人生畏,但事实就是如此。如果这里有人有快速计算这种不明显的时间复杂度的技术,我会很感激他们的意见。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-12
相关资源
最近更新 更多