另一种回溯,排列问题

140 阅读2分钟

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

与子集问题不同的另一种经典问题是求排列。与纯粹的子集问题和最初的笛卡尔积问题不一样,对于排列问题而言,实际上要做的事是在一个不断减少的子集中选取当前位置的元素。

如果我们从m叉树的角度看待这个问题,那么我们可能需要引入一个已访问的标记。这样的话和从dfs的角度是殊途同归的(也就是认为所有的点都是互相连接的,通过标记并取消标记得到不同的生成树)。

但是我们可以有另一种思路,具体来说,对于每个位置,尝试把还没有选取的元素换到当前位置,这是每次选择还没选过的值放到当前位置。此外,在选择下一个值之前,我们要记得撤销本次交换。否则会出现重复的结果。

总而言之,最终我们的代码如下:

def permutation(n: int):
    res = []
    X = [i for i in range(1, n + 1)]

    def perm(d: int):
        if d == n:
            res.append(X.copy())
        else:
            for i in range(d, n):
                X[d], X[i] = X[i], X[d]
                perm(d + 1)
                X[d], X[i] = X[i], X[d]

    perm(0)
    return res

这里有一个添加 X.copy() 原因在于这个值是引用传递,如果是计算一个确定的无重复集合的排列,那么可以由生成的结果保存相关值。

类似地,这类回溯也是有剪枝的。

我们从经典的旅行商问题开始。

由于旅行商问题实际上是枚举一个环,这意味着从任意的顶点出发都会得到等价的结果。因此不妨假设从首个位置开始,也就是不再枚举第一位的值。 而后考虑剪枝的操作,当我们选了下一个到达的城市,如果这一步会导致开销比当前的最优花费还要大,那么就没必要继续搜索了。

因此具体到代码的实现像下面这样:

def tspsolve(g):
    n = len(g)
    x, bx = [i for i in range(n)], []
    bc = float('inf')

    def tsp(d: int, c):
        nonlocal bc, bx
        if d >= n and c + g[x[n - 1]][0] < bc:
            bc, bx = c + g[x[n - 1]][0], x.copy()
        elif d < n:
            for i in range(d, n):
                x[d], x[i] = x[i], x[d]
                if c + g[x[d - 1]][x[d]] < bc:
                    tsp(d + 1, c + g[x[d - 1]][x[d]])
                x[d], x[i] = x[i], x[d]

    tsp(1, 0)
    return bx, bc

注意bx也是使用了copy(),此外由于是环形的路径,最终还要再计算一次结果。