回溯解决方案的大 O 计算范围内的排列

Posted

技术标签:

【中文标题】回溯解决方案的大 O 计算范围内的排列【英文标题】:Big O of backtracking solution counts permutations with range 【发布时间】:2020-10-29 13:05:00 【问题描述】:

我有一个问题,我一直在努力解决解决方案的时间和空间复杂性:

给定一个整数数组(可能重复)Aminlowhigh 是整数。 求A中的项目组合总数:

low <= A[i] <= high 每个组合至少有min 号码。 一个组合中的数字可以重复,因为它们在 A 中被认为是唯一的,但组合不能重复。例如:[1,1,2] -> 组合:[1,1],[1,2],[1,1,2] 可以,但[1,1],[1,1], [1,2], [2,1] ... 不行。

示例:A=[4, 6, 3, 13, 5, 10], min = 2, low = 3, high = 5

在A中有4种组合有效整数的方法:[4,3],[4,5],[4,3,5],[3,5]

这是我的解决方案,它有效:

class Solution:
    def __init__(self):
        pass
    def get_result(self, arr, min_size, low, high):
        return self._count_ways(arr, min_size, low, high, 0, 0)
    def _count_ways(self, arr, min_size, low, high, idx, comb_size):
        if idx == len(arr):
            return 0
        count = 0
        for i in range(idx, len(arr)):
            if arr[i] >= low and arr[i] <= high:
                comb_size += 1
                if comb_size >= min_size:
                    count += 1
                count += self._count_ways(arr, min_size, low, high, i + 1, comb_size)
                comb_size -= 1
        return count

我使用回溯:

时间:O(n!) 因为对于每一个整数,我都会在最坏的情况下检查每一个剩余的整数 - 当所有整数都可以形成组合时。

空格:O(n),因为我最多需要调用堆栈上的 n 次调用,并且我只使用 2 个变量来跟踪我的组合。

我的分析正确吗?

另外,有点超出范围,但是:我应该做一些记忆来改进它吗?

【问题讨论】:

【参考方案1】:

如果我正确理解您的要求,您的算法就太复杂了。你可以这样做:

    计算数组B,包含Alowhigh 之间的所有元素。 为k = min .. B.length 返回sum of Choose(B.length, k),其中Choose(n,k)n(n-1)..(n-k+1)/k!

如果您使用记忆化来计算 Choose 函数的分子/分母,则时间和空间复杂度为 O(n)(例如,如果您已经计算了 5*4*3,则只需一次乘法即可计算 5*4*3*2 等。 )。

在你的例子中,你会得到B = [4, 3, 5],所以B.length = 3,结果是

  Choose(3, 2) + Choose(3, 3) 
= (3 * 2)/(2 * 1) + (3 * 2 * 1)/(3 * 2 * 1) 
= 3 + 1
= 4

【讨论】:

感谢您的回答!我试图做O(n) 以获得lowhigh 之间的数字,但我无法弄清楚第二步。我会尝试实现它。 你有一个链接,我可以找到一种技术来找出你写的公式吗? @Viet Sure: en.wikipedia.org/wiki/Combination 谢谢莫。这很有帮助!【参考方案2】:

您对时间复杂度的分析不太正确。

我知道你从哪里得到O(n!)for i in range(idx, len(arr)): 循环的长度随着每次递归调用而减少,所以看起来你正在做n*(n-1)*(n-2)*...

但是,来自长度为 m 的循环的递归调用并不总是包含大小为 m-1 的循环。假设您最外层的调用有 3 个元素。循环遍历 3 个可能的值,每个值都产生一个新调用。第一个这样的调用将有一个迭代 2 个值的循环,但下一个调用只迭代 1 个值,最后一个调用会立即达到您的基本情况并停止。所以你得到的是((1+0)+1+0),而不是3*2*1=((1+1)+(1+1)+(1+1))

使用大小为 n 的数组调用 _count_ways 所需的时间是使用大小为 n-1 的调用的两倍。要看到这一点,请考虑大小n 调用中的第一个分支,即选择或不选择第一个元素。首先,我们选择第一个元素,这会导致大小为n-1 的递归调用。其次,我们不选择第一个元素,这给了我们n-1 元素进行迭代,所以就好像我们有第二个大小为n-1 的递归调用。

n 每增加一次,时间复杂度就会增加 2 倍,因此您的解决方案的时间复杂度为 O(2^n)。这是有道理的:您正在检查每个组合,并且在一组大小为 n 的集合中有 2^n 组合。

但是,由于您只是尝试计算组合而不是对它们进行处理,因此效率非常低。请参阅@Mo B. 的答案以获得更好的解决方案。

【讨论】:

以上是关于回溯解决方案的大 O 计算范围内的排列的主要内容,如果未能解决你的问题,请参考以下文章

C++ 回溯排列

使用回溯创建给定列表的排列列表:我做错了啥?

回溯---组合

优美的排列--回溯解决

LeetCode 46. 全排列(回溯算法解决)

leetcode 77. Combinations 组合(中等)