简体   繁体   English

使用回溯的所有子集的时间复杂度

[英]Time complexity for all subsets using backtracking

I am trying to understand the time complexity while using backtracking.我试图在使用回溯时了解时间复杂度。 The problem is问题是

Given a set of unique integers, return all possible subsets.给定一组唯一整数,返回所有可能的子集。 Eg.例如。 Input [1,2,3] would return [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] I am solving it using backtracking as this:输入 [1,2,3] 将返回 [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3 ]] 我正在使用回溯解决它,如下所示:

private List<List<Integer>> result = new ArrayList<>();

public List<List<Integer>> getSubsets(int[] nums) {
    
    for (int length = 1; length <= nums.length; length++) { //O(n)
        backtrack(nums, 0, new ArrayList<>(), length);
    }
    result.add(new ArrayList<>());
    return result;
}

private void backtrack(int[] nums, int index, List<Integer> listSoFar, int length) {
    if (length == 0) {
        result.add(listSoFar);
        return;
    }
    
    for (int i = index; i < nums.length; i++) { // O(n)
        List<Integer> temp = new ArrayList<>(); 
        temp.addAll(listSoFar);                 // O(2^n)
        temp.add(nums[i]);
        backtrack(nums, i + 1, temp, length - 1);
    }
}

The code works fine, but I am having trouble understanding the time/space complexity.代码运行良好,但我无法理解时间/空间复杂度。

What I am thinking is here the recursive method is called n times.我在想的是这里递归方法被称为 n 次。 In each call, it generates the sublist that may contain max 2^n elements.在每次调用中,它生成可能包含最多 2^n 个元素的子列表。 So time and space, both will be O(nx 2^n), is that right?所以时间和空间都是 O(nx 2^n),对吗?

Is that right?是对的吗? If not, can any one elaborate?如果没有,谁能详细说明一下?

Note that I saw some answers here, like this but unable to understand.请注意,我在这里看到了一些答案,就像这样但无法理解。 When recursion comes into the picture, I am finding it a bit hard to wrap my head around it.当递归出现在画面中时,我发现我很难理解它。

Your code works not efficiently.您的代码工作效率不高。

Like first solution in the link, you only think about the number will be included or not.就像链接中的第一个解决方案一样,您只考虑是否包含数量。 (like getting combination) (比如获得组合)

It means, you don't have to iterate in getSubsets and backtrack function.这意味着,您不必在 getSubsets 和回溯函数中进行迭代。 "backtrack" function could iterate "nums" array with parameter “backtrack”函数可以用参数迭代“nums”数组

private List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> getSubsets(int[] nums) {
    
    backtrack(nums, 0, new ArrayList<>(), new ArrayList<>());
    return result;
}

private void backtrack(int[] nums, int index, List<Integer> listSoFar) 
// This function time complexity 2^N, because will search all cases when the number included or not
{
    if (index == nums.length) {
        result.add(listSoFar);
        return;
    }
    
    // exclude num[index] in the subset 
    backtrack(nums, index+1, listSoFar)
    // include num[index] in the subset
    backtrack(nums, index+1, listSoFar.add(nums[index]))
}

You're exactly right about space complexity.您对空间复杂度的看法完全正确。 The total space of the final output is O(n*2^n), and this dominates the total space used by the program.最终输出的总空间为 O(n*2^n),这在程序使用的总空间中占主导地位。 The analysis of the time complexity is slightly off though.不过,时间复杂度的分析略有偏差。 Optimally, time complexity would, in this case, be the same as the space complexity, but there are a couple inefficiencies here (one of which is that you're not actually backtracking) such that the time complexity is actually O(n^2*2^n) at best.最佳情况下,在这种情况下,时间复杂度将与空间复杂度相同,但是这里存在一些低效率(其中之一是您实际上没有回溯),因此时间复杂度实际上是 O(n^2 *2^n) 充其量。

It can definitely be useful to analyze a recursive algorithm's time complexity in terms of how many times the recursive method is called times how much work each call does.根据递归方法被调用的次数乘以每次调用的工作量来分析递归算法的时间复杂度绝对有用。 But be careful about saying backtrack is only called n times: it is called n times at the top level, but this is ignoring all the subsequent recursive calls.但是要小心说backtrack只调用了n次:它在顶层被调用了n次,但这忽略了所有后续的递归调用。 Also every call at the top level, backtrack(nums, 0, new ArrayList<>(), length);也是在顶层的每次调用, backtrack(nums, 0, new ArrayList<>(), length); is responsible for generating all subsets sized length , of which there are n Choose length .负责生成所有lengthlength子集,其中有n Choose length That is, no single top-level call will ever produce 2^n subsets;也就是说,没有一个顶级调用会产生 2^n 个子集; it's instead that the sum of n Choose length for lengths from 0 to n is 2^n:相反,从 0 到 n 的长度的n Choose length的总和是 2^n:

在此处输入图片说明

Knowing that across all recursive calls, you generate 2^n subsets, you might then want to ask how much work is done in generating each subset in order to determine the overall complexity.知道在所有递归调用中,您生成 2^n 个子集,然后您可能想知道在生成每个子集时完成了多少工作,以确定整体复杂性。 Optimally, this would be O(n), because each subset varies in length from 0 to n, with the average length being n/2, so the overall algorithm might be O(n/2*2^n) = O(n*2^n), but you can't just assume the subsets are generated optimally and that no significant extra work is done.最佳情况下,这将是 O(n),因为每个子集的长度从 0 到 n 不等,平均长度为 n/2,所以整个算法可能是 O(n/2*2^n) = O(n *2^n),但您不能仅仅假设子集是最优生成的,并且没有完成任何重要的额外工作。

In your case, you're building subsets through the listSoFar variable until it reaches the appropriate length, at which point it is appended to the result.在您的情况下,您正在通过listSoFar变量构建子集,直到它达到适当的长度,然后将其附加到结果中。 However, listSoFar gets copied to a temp list in O(n) time for each of its O(n) characters, so the complexity of generating each subset is O(n^2), which brings the overall complexity to O(n^2*2^n).然而, listSoFar在 O(n) 时间内为每个 O(n) 字符复制到临时列表,因此生成每个子集的复杂度为 O(n^2),这使整体复杂度为 O(n^ 2*2^n)。 Also, some listSoFar subsets are created which never figure into the final output (you never check to see that there are enough numbers remaining in nums to fill listSoFar out to the desired length before recursing), so you end up doing unnecessary work in building subsets and making recursive calls which will never reach the base case to get appended to result , which might also worsen the asymptotic complexity.此外,创建了一些listSoFar子集,它们永远不会出现在最终输出中(在listSoFar之前,您永远不会检查以查看nums是否有足够的数字来将listSoFar填充到所需的length ),因此您最终会在构建子集时做不必要的工作并进行永远不会达到基本情况的递归调用以附加到result ,这也可能会恶化渐近复杂性。 You can address the first of these inefficiencies with back-tracking, and the second with a simple break statement.您可以通过回溯解决这些低效率中的第一个,然后使用简单的 break 语句解决第二个问题。 I wrote these changes into a JavaScript program, leaving most of the logic the same but re-naming/re-organizing a little bit:我将这些更改写入了 JavaScript 程序,大部分逻辑保持不变,但稍微重新命名/重新组织:

 function getSubsets(nums) { let subsets = []; for (let length = 0; length <= nums.length; length++) { // refactored "backtrack" function: genSubsetsByLength(length); // O(length*(n Choose length)) } return subsets; function genSubsetsByLength(length, i=0, partialSubset=[]) { if (length === 0) { subsets.push(partialSubset.slice()); // O(length): copy partial and push to result return; } while (i < nums.length) { if (nums.length - i < length) break; // don't build partial results that can't finish partialSubset.push(nums[i]); // O(1) genSubsetsByLength(length - 1, ++i, partialSubset); partialSubset.pop(); // O(1): this is the back-tracking part } } } for (let subset of getSubsets([1, 2, 3])) console.log(`[`, ...subset, ']');

The key difference is using back-tracking to avoid making copies of the partial subset every time you add a new element to it, such that each is built in O(length) = O(n) time rather than O(n^2) time, because there is now only O(1) work done per element added.关键的区别是使用回溯来避免每次添加新元素时都复制部分子集,这样每个元素都是在 O(length) = O(n) 时间而不是 O(n^2) 中构建的时间,因为现在每个添加的元素只完成 O(1) 工作。 Popping off the last character added to the partial result after each recursive call allows you to re-use the same array across recursive calls, thus avoiding the O(n) overhead of making temp copies for each call.在每次递归调用后弹出添加到部分结果的最后一个字符允许您在递归调用中重复使用相同的数组,从而避免为每次调用制作temp副本的 O(n) 开销。 This, along with the fact that only subsets which appear in the final output are built, allows you to analyze the total time complexity in terms of the total number of elements across all subsets in the output: O(n*2^n).这与仅构建出现在最终输出中的子集这一事实一起,允许您根据输出中所有子集的元素总数来分析总时间复杂度:O(n*2^n)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM