分享一下深度优先搜索 LeetCode 做题总结。
深度优先搜索(Depth-First Search)
- 常见使用场景
- 求子序列、求子集
- 找所有可能的解决方案,逐层组装结果(跟第一种没太大差别)
leetcode具体例子
深度优先,一直往下搜索到最终结果后返回
逐层组装结果到最深一层:求子集、求序列
- Subsets 每个节点路径都是结果:第一层只有 0 个元素,逐层从给定的数组中取出元素添加,每个节点都作为结果添加到数组中,直到根节点。查看代码
- Subsets II 每个节点路径都是结果:给定结合内容排序,然后在上题 Subsets 的基础上增加筛选条件,不能同前面的元素一样。查看代码
- Letter Case Permutation 叶子节点是结果:从第一个元素开始添加结果,如果遇到字母则进行大写字母转换,小写字母转换(同时都做,因为大写做转大写操作依然是大写,小写转小写操作依然是小写,不会有影响),然后再添加后面元素。查看代码
经历了一个多余复杂的代码过程,时间效率和代码都差 1 倍: 第 1 题是每层的变化是集合中元素个数,本题变为修改的元素个树,顶层修改元素个数为 0,往下逐层增加 1。规则同第 2 题,第 2 题只能添加后面的元素,本题只能修改在前面已更改元素后面的字母。有个坑跟前面 2 题是有差别的:
- 其实前面的分析是不对的,这里有个坑就是题目给的集合顺序,字母是从后往前修改的,所以循环需要用倒叙,只能修改上一次修改过位置的前面位置。
- 数据顺序是一层一层读的,需要一层一层往上返回结果。当层的结果拿到,下一层的结果拿到,然后两者合并进行返回。 教训:只管变化的内容就好,不变化的内容传来传去费劲儿。
- Permutations 叶子节点是结果:每一层在上一层的基础上增加一个前面没有使用过的元素。查看代码
找所有可能的解决方案
- N-Queens 叶子节点是结果:下一个节点需要考虑前面的节点没有在冲突位置。挑战点:斜角的冲突位置计算,可以通过横纵坐标差或者和来判断,因为 [x - minus, y - minus] 以及 [x + minus, y - minus] 的两种位置会与当前要存放的 [x,y] 节点冲突。查看代码
- Palindrome Partitioning :
- 方法1: 回文串内容作为节点。
- 方法2: 分割线作为节点,部分节点路径都是结果。需要关注的是结果分析,(1)只有部分节点路径是结果,筛选条件需要判断,前后分割都是回文串;(2)DFS 筛选条件:前面分割已经是回文串的才有必要继续深入搜索。(3)0 分割线问题需要单独处理。
- 查看代码
- Combination Sum:套用模板即可,不过需要注意 sum 的回溯。查看代码
- Combination SumII: 在3题的基础上增加排序,以及筛选掉重复元素。
i !== pos && candidates[i] === candidates[i - 1]时候直接下一个函数。查看代码
DFS 分析步骤总结(最好先画图构建搜索树)
- 「解决方案」确定能否用 DFS:常见子序列、子集合、求所有解决方案用 DFS
- 「节点」确定节点,先看看输出结果最小单位内容能不能作为节点,如果不能,找一个转换方式
- 「扩展筛选条件」哪些节点是可以继续搜索的
- 「节点扩展」如何从上一层节点扩展到下一层
- 「结果分析」什么时候产出结果,停止继续搜索。分析结果是每个节点,还是每个节点路径,还是叶子节点,还有可能是部分节点、部分节点路径,如果是部分,则需要增加筛选条件。
- 「结果转化」节点可能需要转化成其他形式,成为题目所需要的结果。可以在结果阶段统一转换,也可以在加入结果时一步步进行转化。
- 套代码模板
DFS 代码基础模板总结
function func (originData) {
// 存储最终结果
const result = []
// 深度优先搜索,搜索节点
dfs([])
// 必须入参为前一个节点数值,初始化值是根节点,以空字符串或者空数组为主
// 视情况而定如参:如增加筛选条件的下标
function dfs (current)) {
// 如果结果是所有节点,则直接将 target 加入到结果中。
// 注意1: 如果是对象,最好重新构造一个,避免对存储结构中的数据造成影响:可以在加入时重建,也可以在传参时进行。
// 注意2: 如果结果是叶子节点或者其他条件,结果增加筛选项目,也可以在回调前增加筛选项目。
// 注意3: 可能存在结果需要转化的,特别是整体结果转化的放在这里。单个转化的建议放在传参部分。
result.push(new Array(current))
for (let i = 0; i < originData.length; i++) {
// 注意3: 这里可能需要进行数据转化,单个转化建议放在此处,如果是全部转化
// 注意4: 这里传给 DFS 的所有结果都要回溯,因为 originData[i + 1] 也是在 current 基础上进行的。包括与 current 相关的结果已经把当前节点加进去的也要回溯
current.push(originData[i])
dfs(current)
current.pop()
// 避免回溯的简单写法如下
// dfs([...current, originData[i]])
}
}
return result
}
可能挑战点分析
- 节点分析,节点可能需要通过现象看本质,不单单是简单给的数字,例如回文串分割节点是分割线
- 节点间演算条件,要考虑充分,避免遗漏,例如 N 皇后问题斜角也不能放置,这里的斜角代码还有点小技巧