进一步的利用回溯

166 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情

前面我们讨论了笛卡尔积的递归算法实现。现在我们将要进一步的使用这个方法。

子集的生成

子集的问题实际上和笛卡尔积是同一个问题,因为子集实际上可以看作枚举 R ^ n ,其中 R 包括 0、1,而 n 代表集合元素的个数。

于是我们可以写出如下的代码:

def subset(n: int) -> List[List[int]]:
    res = []
    mask = [False] * n

    def bt(d: int):
        if d == n:
            res.append([i for i, x in enumerate(mask) if x])
            return
        mask[d] = True
        bt(d + 1)
        mask[d] = False
        bt(d + 1)

    bt(0)
    return res

其中 mask 表示已经被枚举的结果,相当于我们枚举的笛卡尔积。不过由于子集问题我们需要的是部分选择,从这个角度我们也可以认为只是选择一部分的数据,这和我们所谓的掩码是近似的概念,只需要一部分,不关注其他的部分。所以最终我把它命名了。

另一个值得注意的是,和与先前笛卡尔积不同的是,我们没有写成循环的形式,而是写了两份。

这里并不是因为它的分支少所以展开写或者使用循环等价。考虑的思路是对更具体的子集问题,实际上会有一些减枝操作。

而当前的这个位置做出决策时考虑的是不完全一样的,选择的时候我们更多的考虑他是否可以选择,而不选的时候更多地是考虑还能否有结果。这么说有些抽象,等到后序更具体地例子我们在解释相关的原因。

另外还有一点值得注意的是,对于回溯算法而言,最终的结果复杂度实际上比循环还要高一个量级。

比如说上面的子集生成算法,每个位置的选和不选构成n次操作。但是在每次生成子集时需要遍历整个子集,也需要遍历n个元素。最终复杂度的数量级在于n2nn2^n