0%

回溯

二叉树的路径
LC257 二叉树的所有路径
LC112 路径总和
LC113 路径总和II
LC437 路径总和III
LC129 根到叶子组成的数字

组合
LC77 组合
LC40 组合总数II

简单回溯
LC93 复原IP地址
LC17 电话号码的字母组合
LC22 括号生成

困难回溯
LC10 正则表达式匹配
LC44 通配符匹配
LC79 单词搜索
[LC894 所有可能的满二叉树]

回溯是深度优先搜索决策树的过程。回溯的基本组件:

  • 选择列表:当前可以选择的选项列表,可能因为当前路径而有所改变。因为对于当前路径而言,有些选择可能不合法。
  • 当前路径:记录当前已经作出的选择的列表。
  • 结束条件:判断当前路径是否是结果的函数。
  • 结果集合:记录所有合法结果的集合。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    res = []
    path = []
    backtrack(path, decision_list)

    if (isResult(path))
    res.push_back(path)
    return

    for decision : decision_list
    if(!isValid(path, decision))
    continue
    path.push_back(decision)
    backtrack(path, decision_list)
    path.pop_back

二叉树的路径

二叉树除了根节点以外,每个节点都只有双亲节点。因此从根节点到任意节点都只有一条路径。从任意节点到根结节点也只有一条路径。这类题目的思路基本都是回溯。

二叉树的所有路径

LC257 二叉树的所有路径

思路:

  • 对二叉树进行回溯
  • 回溯结束条件为当前节点为叶子节点。
  • 麻烦的地方可能在于"->"的放置。所有节点都要将自己的节点值root->val加入到path中,但叶子节点不需要加"->",而且叶子节点需要将path放入结果集合里。而非叶子节点需要加"->"。此外,不同长度的数字,向path里放入的长度也不同。这导致了回溯时path需要删掉几个字符造成了不一致。所以我们需要记录一下我们往path里加了几个字符。

路径和

LC112 路径总和

题目要求判断二叉树中是否存在一条从根节点到叶子节点,和为指定值的一条路径。

  • 回溯过程中记录当前路径上的和。
  • 结束条件为当前节点是叶子节点,且当前路径和为指定的值。

优化:设置sum初始值为目标值,并不断的向下减去当前节点的值。


LC113 路径总和II

题目要求找到二叉树中所有从根节点到叶子节点,和为指定值的一条路径。思路仍然和前一题一样:

  • 回溯过程要记录当前路径 和 当前路径和(不然每次抵达叶子节点,都需求一次路径和)。
  • 结束条件为当前节点是叶子节点,且当前路径和为指定的值。

LC129 根到叶子组成的数字

需要二叉树中所有的路径形成的数字的和。将双亲节点传来的数字乘10后加上本节点的数字即可。


LC437 路径总和III

题目要求找到二叉树中所有和为指定值的一条路径。路径不一定从根节点出发,也不一定在叶子节点结束,但路径方向一定是向下的(即不能从某一点处折返)。

思路一:

  • 对树的每一个节点都进行一次回溯
  • 回溯的结束条件为路径和为指定值

思路二:

  • 二叉树中从根节点到任意节点都只有一条路径。
  • 不要求起点,也不要求终点,考虑这是一个区间和问题。区间和问题的解决手段往往都是前缀和
  • pathsum为想要的区间和。cursum为根节点到当前节点的和。
  • 回溯过程中,使用一个hashmap记录路径上的前缀和。hashmap的key为前缀和,hashmap的val为该前缀和的数量。
  • 更新结果:在当前节点判断hashmap中是否有key的值等于cursum - pathsum。(因为区间和等于前缀和的差,即cursum - key == pathsum)有,则说明找到了val条和为sum的路径。
  • 更新hashmap:
    • 如果当前节点不是叶子节点,检查hashmap中是否有key等于当前节点处前缀和的,如果有就给key对应的val加1。如果没有就创建这样的一个key,对应val为1。
    • 如果当前节点是叶子节点,就没必要再更新hashmap了,因为不会有从当前节点往下的递归调用了。

注意:
在当前节点除了判断是否有key == cursum - pathsum,还需要判断一下是否cursum == pathsum,这也是一个解。如果不想判断cursum == pathsum,也可以在hashmap中加入一个(0, 1)的key-value对。表示到根结点的区间和。

组合

LC77 组合

可以用回溯的方式。用k限制回溯树的深度。


LC39 组合总数

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。所有数字(包括 target)都是正整数。解集不能包含重复的组合。

根据要求我们需要给结果集合去重。我们可以得到所有结果后对结果按字典序排序然后去重。更好的做法是在进行回溯的同时就进行去重。我们可以通过保证决策树的单调性来达到去重的目的(//TODO: 为什么?)。不允许深层作出的决策大于浅层作出的决策。

  • 将candidates中的数组排序。
  • 在遍历candidates数组的时候,传给下一个candidate的选择列表缩小1。(见代码)

优化:

  • 在判断是否为有效的路径时,因为candidates已经排序,如果当前的candidate无效,后面的所有candidates > candidate,因此也是无效的。此时直接break循环即可。

LC40 组合总数II

和前一题基本一致。虽然数组总有重复的数字,但是不允许同一个数字被重复使用。所以除了通过排序去重外,还需要在每次的选择列表中去重。如果这一次的选择和上一次相同,那么就跳过。


简单回溯

复原IP地址

LC93 复原IP地址

简单回溯。需要注意的是”00”不是一个有效的IP段,因此如果遇到第一个数就为0时,不应该继续向下分割字符串,否则会出现前置的0。

电话号码的字母组合

LC17 电话号码的字母组合

简单回溯。没有不合法的选择。注意结束条件满足,将path加入结果集合后要return。

括号生成

LC22 括号生成
简单回溯。每次决策只有两种。
注意:

  • 回溯算法修改状态后,无论怎样都要恢复状态。除非你用函数栈来复制参数。

困难回溯

正则表达式匹配

LC10 正则表达式匹配

比较复杂的回溯。恢复状态比较麻烦,所以我们直接借助函数调用栈来拷贝参数。

  • 如果p为空:
    • s为空,匹配成功
    • s不为空,匹配失败

[以下开始p非空,但s是否非空不确定]

  • 如果p的第二个字符是*
    • 我们可以忽略p的前两个字符,匹配s和p.substr(2)
    • 如果s非空,我们可以检查p的第一个字符和s是否匹配,如果匹配,我们就进一步去匹配s.substr(1)和p
    • 如果以上两种情况都未匹配成功,则匹配失败。
  • 如果p的第二个字符不是*
    • 如果s非空,我们检查p的第一个字符和s是否匹配,如果匹配,我们就进一步去匹配s.substr(1)和p.substr(1)
    • 否则匹配失败。

通配符匹配

LC44 通配符匹配

需要进行仔细剪枝的回溯,否则会导致TLE。暂且不知道该如何剪枝。

可以通过贪婪的方式进行匹配(未证明,但是AC):

遇到相同字符就直接匹配。
遇到通配符就不断扩大匹配长度直到匹配成功或者就算搜寻到匹配字符的尾没成功,那就匹配失败。

  • 遇到相同字符 或 模式字符为'?'就直接匹配。
  • 遇到模式字符为'*',就记录下此时模式字符'*'的位置pStar和对应的匹配字符的位置iStar,将模式字符的指针向前移动(即尝试将'*'和空串匹配),回到前一步。
  • 如果曾经记录过模式字符中'*'的位置,但在第一步中匹配失败,说明'*'匹配的长度不够。我们应该增加'*'匹配字符串的长度,即将iStar向前移动,并将i回溯至iStar位置,j回溯至jStar + 1位置。
  • 如果未曾记录过模式字符中的'*'却匹配失败,那就彻底失败了。

单词搜索

LC79 单词搜索

要求:同一个单元格内的字母不允许被重复使用。因此我们需要一个和原矩阵一样大的矩阵explored来记录哪些格子已经被探索了。
初始化一个和原矩阵一样大的矩阵,并标记所有元素为NOT_EXPLORED。在回溯函数中,注意标记EXPLORED和标记NOT_EXPLORED一定要成对出现,这样可以保证在[i, j]位置的一次探索结束后,explored仍然能恢复成全为NOT_EXPLORED的样子。

回溯函数传递当前探索的坐标[x, y],已经查找到单词的第n位。

失败的情况:

  • 如果 n > word.size() - 1,则已经无需再探索,返回false
  • 如果 board[x][y] != word[n],说明这一位找错了,返回false
  • 如果 explored[x][y] != NOT_EXPLORED,说明走了回头路,返回false

成功的情况:

  • 如果 n == word.size() - 1 && board[x][y] == word[n],证明正好找到了,返回true

其他(继续探索的情况):

  • 标记该点为已探索
  • 探索上下左右,只要有一个成功,那就成功了,剩下的就不要再去探索了。
  • 标记该点为未探索
  • 返回结果。

所有可能的满二叉树

LC894 所有可能的满二叉树

  • 满二叉树的节点个数一定为奇数
  • 欲获得节点数为N的满二叉树:
    • 其左子树为节点数为$i = 1,3,5, \cdots ,N - 2$的所有可能满二叉树的集合
    • 当其左子树节点数为i时,其右子树的节点数必然为$N - 1 - i$,因为总节点数$N$ - 满叉树的根节点$1$ - 左子树的节点数$i$ $=0$
  • 对所有可能性进行枚举即可。