LeetCode 回溯问题专题及总结

代码

回溯问题专题及总结,以八皇后和走迷宫这两个经典的回溯问题开始。其余题目只有 LeetCode medium 难度,包含了当前的所有 Medium & Backtracking 问题,使用 python3 的 jupyter notebook 编写,详细代码请看 my github

总结

回溯问题模板:

  1. 定义一个全局变量,用以添加回溯过程中找到的一组解

  2. 定义回溯函数:

    2.1 回溯函数参数:
    * 局部变量 path 存储找到的一组解, 这个参数一般是必须的
    * 记录当前位置的变量:如 start、x 、y 等,视具体问题而定

    2.2 回溯函数结束条件: 回溯本质是递归,递归就要有结束条件,不然出不去;结束条件一般比较好想,通常是边界条件,比如数组为空、当前位置到最后一个位置、或者当前 path 中的元素个数满足条件了等。

    2.3 如何表示回溯:不能改变局部变量 path 的值才能实现回溯:
    * path.append(X); back(path); path.pop() , 对于 back 函数是沿着一条路深入,所以要加上当前点,path.pop()是恢复原来路径,去找同一层的下一个点
    * 直接 back(path + [X]),在 path 后加上当前的点,没有修改 path,和上面其实是一样的

    2.4 满足某种条件进行剪枝:通常当前位置往下一步走可能有多个选择,但是有些选择点是明显不满足条件的,没必要进行尝试,通过合理的条件判断跳过这些点,可以极大的缩短查找空间,减小时间复杂度,这一步做的好在数据量大的时候就能 AC 了或者 beats 更多人,不过这个条件要自己去摸索了。

python 模板:

final_result = []
def back(path, start):
    if 结束条件:
        final_result.append(path)
        return
    for 候选点集合:
        if valid(候选点):
            back(path + [候选点], 新位置) 
back(path=[], start=0)
print(final_result)

回溯问题类型整体可以分为三类:

  1. 找所有层上的子集:例如 Subsets I 和 Subsets II,特点是每个子集的长度不一样。这中问题没有显式的结束条件,因为每一层上的子集都是合理的,
    所以要在回溯函数开头就加上当前解:
def back(path, start):
    final_result.append(path)
    for 候选点集合:
        if valid(候选点):
            back(path + [候选点], 新位置)
  1. 找所有到最后一层的解:例如全排列问题、八皇后问题,特点是每个解的长度是一样的,每次递归沿一条路走到最后,到叶子节点的整个路径是要找的一组解,
    这时候就需要有显式的结束条件:通常是到达边界处作为结束条件 然后 return
def back(path, start):
    if 结束条件:
        final_ryesult.append(path)
        return
    for 候选点集合:
        if valid(候选点):
            back(path + [候选点], 新位置) 
  1. 找到满足某些条件的总个数,如 357. Count Numbers with Unique Digits, 526. Beautiful Arrangement等,虽说也可以用上述框架,使用全局变量 final_result 存储每个局部解,然后返回 len(final_result) 就是全部解的个数,但是这样会浪费存储空间,因为并不要求返回全部解。我们可以
    在递归的时候直接返回个数。这时候结束条件就不是一个光杆司令 return 了,需要返回一个数,如 return 1 之类。

    刚说到 for 中的每个候选点是处在同一层的,而回溯的递归是沿着其中一个候选点深入下去,返回这一个分支的解的个数,那么横向这一层下所有解的个数就是各个候选点的回溯结果相加,总结如下:

def back(start):
    if 结束条件:
        return 1 # 举例,实际问题中未必是 1
    total = 0
    for 候选点集合:
        if valid(候选点):
            total += back(新位置) # back 就是一个分支递归下去返回的结果,如果递归不好理解,可以想象成这就是一个普通函数
    return total # 最后返回当前这一层的所有解

具象-抽象

有时候一时转不过弯或者不知道什么时候 return,不知道 valid 条件怎么写,这时候就找几个具体例子,比如从第一个位置开始,按照题意是什么情况,从第二个位置开始是什么情况,找几个具体的例子,观察一下规律,抽象出几个变量不知不觉中就可以套入到这个模板中了。

往往 for 这一层循环的是所有的候选点,它们是处在同一层的,for 中的一个满足条件的点再到 back 函数中,这是一条路继续往下走,其实就是 DFS,见下图示,刚开始节点是空,然后红色框代表每一层的候选点也就是 for 的部分,绿色框代表每个路径深入下去直到叶子节点,叶子节点就是 return 结束条件。建议刚开始拿到题目时候,按照下面这个图列举几个例子,就很容易找到其中的规律了。
LeetCode 回溯问题专题及总结_第1张图片

你可能感兴趣的:(Python)