leetcode/notes/src/backtrack.md

2.5 KiB
Raw Blame History

总结

使用场景:

  • 如果解决一个问题需要多个步骤,而每个步骤有多个可能的结果,题目又要求我们找出所有可能的结果,那么这个时候可以考虑回溯法。
  • 回溯法本质是在一棵树上进行深度优先遍历

算法设计:

关键就是要学会分析和画图,然后确定传入参数。只要传入参数确定了代码框架就确定了 (返回值一般是 void)

  • 思考这棵树怎么画,每层遍历的逻辑是什么,每条边的操作逻辑是什么。
  • 得设计一个数据结构 NodeState 来存放当前节点状态。该数据结构的可扩展性必须要强,需要满足以下条件:
    • 能描述当前节点的状态
    • 能作为最终结果存储
    • 能根据当前节点更新状态和撤销之前的更改
  • 得有一个 &result 来存放结果,这个 &result 通常是一个向量 vector<NodeState> &,里面存放了节点状态。
  • 其它传入参数用来完成每层遍历操作和每条边的操作。

设计完了数据结构之后来看看具体代码怎么写。模板如下:

void backtrack(NodeState &node, vector<NodeState> &result, int para1, int para2, int para3) {
  // 终止条件
  // 回溯法中的每个节点并不是真的树状节点,没有 `nullptr` ,因此用空指针来判断是否到了叶子结点并不合理,需要其它的一些方法来确定是否到达叶子节点,比如高度。
  if (/* end condition */) {
    result.push_back(node);
    return;
  }

  // 遍历该节点的所有子节点,即遍历下一层
  for (...) {
    // 剪枝
    // 当现在的节点不可能出现我们想要的结果时,直接跳过。
    if (/* out of scope */) {
      continue;
    }
    // 处理节点
    // 现在 node 中的数据描述的是当前节点,
    // handle(node) 一般是让 node 中的数据变成子节点的数据
    handle(node);
    // 递归
    backtrack(node, result, para1, para2, para3);
    // 撤销数据处理,让 node 中的数据再次变回描述当前节点的数据
    revert(node);
  }
}

复杂度分析:

  • 时间复杂度:最长路径长度 × 搜索树的节点数
  • 空间复杂度:一个节点所需要的空间 × 搜索树的节点数

分类:

  • 组合问题N 个数里面按一定规则找出 k 个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个 N 个数的集合里有多少符合条件的子集
  • 排列问题N 个数按一定规则全排列,有几种排列方式
  • 棋盘问题N 皇后,解数独等等