51. N 皇后
-
DFS + 回溯 :由于 NxN 个格子放 N 个皇后, 同一行不能放置 2 个皇后,所以皇后必然放置在不同行 。
-
因此,可以从第 0 行开始,逐行地尝试,在每一个 i 行 中,每一个列位置 j 上放一个皇后,但在放之前需要判断该位置放了的话是否会产生冲突(即是否会相互攻击):
-
判断冲突的条件是 该位置所在列、所在主对角线、所在次对角线上是否已经放置了皇后 ,
-
1)如果冲突,就尝试下一个列位置(continue循环),
-
2)如果不冲突,则该行的 j 位置能够放下一个皇后,同时 标记该位置所在列、所在主对角线、所在次对角线上已经放置了皇后 ,并保存放置的 皇后位置 结果, 然后 递归调用的下一行 dfs(i+1) 去放置皇后。
-
注意:从下一行递归调用返回之后,需要做 回溯处理 ,将之前标记为 true 的皇后位置,再标记为 false ,同时撤销之前保存的 皇后位置 结果 。
-
递归终止:递归参数行号 row==N 时,得到一种可行的答案,将当前收集的答案添加到 总结果集 中并返回。
-
在递归过程中的 保存 结果时,使用一个长度为 N 的数组 int[] queens ,表示每一行中 皇后所在的列下标,每当递归行号等于 N 时,再将 queens 输出到总集 res 中。也就是说我们是按行收集答案的,因此当到达最后一行时便得到一种解法,再加入总答案即可。
-
本题判断放置皇后冲突的 关键 条件是两个皇后不能同时处于同一行、同一列、同一斜线中。
-
由于我们是按照行放的,而每一行中只会尝试放一个列位置,因此第一个条件通过递归即可满足,所以只需关注后两个条件,可以使用 3个 boolean[] 数组 来标记 所在列、所在主对角线、所在次对角线 上是否已经有皇后,
-
关键是对角线数组的下标计算: NxN 的矩阵总共有 2xN-1 条对角线,对于坐标 [i, j] 对应的主对角线下标为 [N - i + j - 1] (或 [N + i - j - 1] ), 对应的次对角线下标为 [i + j] 。
代码:
37. 解数独
-
1. DFS 本题解法思路与51. N 皇后类似,
-
首先创建 3个boolean数组 boolean[9][10] 、 boolean[9][10] 、 boolean[3][3][10] 分别用来标记数字 [1-9] 在对应的 行 、 列 及 3x3的宫格 中是否使用过。(数组的最后一维下标 [0-9] 中 0 位置不使用,只用 [1-9] 下标来对应数字 [1-9] )
-
先遍历一遍矩阵,将已有的数字在上面 3个boolean数组 中 标记 清楚。
-
然后进行 dfs(0, 0) 递归调用 ,表示从 [0,0] 位置开始出发,从左往右填充每一行中的每一列的格子,在每次递归中判断:
-
如果当前格子是 数字 ,直接返回递归调用下一个格子 dfs(i, j+1) 的调用结果。
-
如果当前格子是 空格 ,遍历数字 [1-9] 尝试填充每一个数字 ,如果当前格子坐标 所在行、所在列、所在3x3的box 中都没有放过该数字,那么就 标记为已放置并填充数字到当前格子里 ,然后递归调用当前行的下一个格子 dfs(i, j+1) 。在递归返回之后,如果返回的是 false 需要 回溯撤销 操作,将之前标记的改回去,并将格子重置为空格。如果返回的是 true ,那么就不需要继续了,直接返回即可。
-
递归终止:当行号 i==9 时,返回 true ,表示放完了所有的行,有解,返回。
-
每次递归时需要首先判断列号 j == 9 时 ,需要 换行 i++ , 从下一行的第一列 j = 0 开始。判断换行的逻辑不能放在 if (i == 9) 的判断之后进行,这是因为换行之后 i 可能已经是 9 了。
-
递归传递坐标 [i, j] 向下递归调用时, 列 j+1 , 行 i 不用 +1 , 因为 行是在本次递归中根据列走到头再执行换行 i++操作的 。
本题与51题的区别:
- 51 题是在递归向下一层传的时候进行 i + 1,因为 51 题中同一行中不能同时放2个皇后,所以是按照行递归下去,而当前行的某一列放置了皇后之后,其余的列就不需要考虑了(因为当前行中只能放一个皇后),所以不需要将列坐标带到下一次递归调用中。
- 本题虽然也是按照行递归处理,但其实是按照每一行中从左到右逐列处理的,因为相当于是做填空题,每一行中的每一个空格都要填充完毕,所以递归函数是带了 i 和 j 的。如果当前行的当前列是数字,则直接递归调用当前行的下一列,如果当前行的当前列是空格,就遍历1-9进行填充,每次填充当前列之后都要递归调用下一列。与 51 题相比,本题的换行操作是在每次递归调用中完成的,而不是通过 i + 1 向下传递的方式。
-
2. 在每次递归函数中遍历9x9的矩阵,寻找一个空格来填充 [1-9] ,如果找到了,就标记并填充,然后递归调用,如果子递归返回 true ,就返回 true ,否则进行回溯。 在 [1-9] 循环完后,说明当前的格子无法被填充,返回 false 。
-
最终如果9x9的矩阵遍历完了,也没有返回 false ,递归函数就返回 true 。
这个方法就相当于是有点暴力了,相比方法1,它没有将坐标[i, j]递归往下传,而是在每一次递归调用中都扫描整个二维矩阵,以寻找一个空格进行尝试插入数字[1-9],由于题目的数据量是9x9的,比较小,所以这样也能过,但是如果数据量比较大,时间复杂度就会上去,因为每次递归中都是O(N^2)。
679. 24 点游戏
-
DFS ,整体思路:在每层中从上层传递的结果数组中任意选取 2个 数进行加减乘除计算,得到的结果和剩余数字组成新的结果数组向下传递,这样递归下去,最终会计算到列表中只剩 2个 数字,最后将会到达叶子节点,只剩一个数字,此时判断是否是24即可,也就是说在一棵树中进行DFS查找满足节点值是 24 的叶子节点。
-
具体地,首先将原始数组加入到 double[] arr 拷贝中,然后调用递归函数 dfs(arr, level) ,初始 level=0 ,递归函数返回值表示是否能得到值为24的结果。
-
在每次递归中, 双层for循环 遍历 [i, N] 和 [j, N] ,从中选择两个下标 i 和 j , 其中 N 是当前 arr 的长度,然后创建一个 double[N - 2 + 1] childArr 作为下一层需要的结果(当前层消耗2个数增加1个结果),将除 i 和 j 以外的数放入 childArr 中,然后对 i 和 j 分别选取 +-*/ 四种操作符进行计算,并将结果放入 childArr 中进行递归调用,如果在某一个操作符计算结果后,递归返回的结果为 true ,则可以终止,不需要继续进行了。
-
递归终止: level == 3 时,判断当前 arr[0] 是否等于 24 。
-
剪枝点1: i==j 时,自己不能跟自己进行运算,跳过;
-
剪枝点2: 加法和乘法满足交换律 , 可以跳过 i > j 的部分 ,避免重复计算;
-
剪枝点3: 除数不能为 0 ,当操作符是 / 时,跳过 j 的值是 0 的情况。
-
另外,题目中的除法为实数除法,是带小数点的浮点类型,因此在判断结果是否等于 24 和判断除数是否等于 0 时,需要判断绝对值误差是否小于 10^(-6) 。
注意:这个代码每次递归中是将当前选的两个数计算结果和剩余数字组成一个新数组往下一层递归传递,当然你也可以用List来存储每一层的计算结果,如果你用List,则在递归调用返回之后,需要做回溯处理,即删除刚刚添加的计算结果,再继续处理下一个操作符。(可以使用LinkedList当做栈使用)
351. 安卓系统手势解锁
-
DFS: 对 [m, n] 闭区间上的每一种长度 len 进行DFS递归统计种数,所有种数相加就是答案。
-
递归函数 dfs(last, len) 含义:返回从 last 点出发,形成长度为 len 的有效手势的种数。初始调用 last 传 -1 。
-
在每次递归调用中,从 [0, 8] 的方格中任意选择位置 i ,判断 i 是否能够与 last 位置形成有效手势,
-
如果可以形成有效手势,通过标记数组 used 标记为使用过,然后递归调用 dfs(i, len - 1) 继续判断从 i 位置出发,寻找 len -1 长度的有效手势方法数,并将返回结果累加到当前调用层的结果中。
-
注意:在递归调用返回之后,做 回溯处理 , used 数组重新标记为 false 。
-
递归终止:长度 len 减到0 时,得到 1 种有效手势,返回 1 。
-
i 与 last 相同,跳过,即同一个点;
-
used 数组为 true ,跳过,即访问过了;
-
last 不是 -1 时,判断 i 和 last 能形成有效的 手势 ,才继续,否则跳过。
-
1)两个数之和为奇数: (i + j) % 2 == 1 这是属于同一行或同一列中相邻的元素或者不穿过其他数字中心的斜线上的两个元素(一个在角上一个在斜对边的中心),合法;
-
2)两个数之和为8: (i + j) / 2 == 4 这是属于经过 3x3 方阵的正中心的元素,需要确保正中心数字被访问过才可以 used[ mid ] == true ;mid = (i + j) / 2
-
3)处于对角线上的相邻元素: i % 3 != j % 3 且 i / 3 != j / 3 ,合法;
-
4)其余情况:处于四条边上的两个角的点,需要中心元素被访问过才行 used[ mid ] == true 。
len的含义可以理解为长度,也可以理解为点数,即从 i 出发,经过 len 个点可以构成有效手势的前提是经过前面 len - 1 个点也能构成有效的,而经过前面 len - 1 个点可以构成有效的前提是经过前面 len - 2 个点也可以构成有效的...直到点数减少到0,说明得到了一条从 i 出发的路径,该路径上经过的 [1, len] 点数范围都可以构成有效手势。这条路径即是从 i 出发的一种解法。把从 [0, 8] 出发的每个 i 的解法累加就是当前经过 len 个点的解法,把每种 len 的解法累加就是区间 [m, n] 总的解法数。
这里如果将dfs的返回值改成boolean类型,然后使用全局变量res进行累加可能更好理解一点:
130. 被围绕的区域(岛屿一类问题)
-
1. DFS ,
-
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,且不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。
-
也就是说只存在两种 'O' : 被 'X' 包围的 'O' 和不被 'X' 包围的 'O' ,而不被 'X' 包围的 'O' 都在边上 。 那么 只要通过DFS或BFS把边界上连通的 'O'替换成其他字母,然后循环遍历把剩下的'O' 换成 'X' ,再把边上的 'O' 还原回去即可 。
-
问题转换为 寻找以每一个边界上的 'O' 为起点的连通的 'O' 。
-
首先遍历二维矩阵找到每一个 board[i][j] 是 'O' 且 [i, j] 是 位于边上 的位置出发进行一次 dfs 调用。
-
在递归函数中,如果遇到 'O' 就将 [i, j] 位置字符改成 '#' , 然后 上下左右 四个方向进行 DFS ,
-
递归终止: i 或 j 越界,或者 [i, j] 位置不是 'O' ,返回。
-
递归处理完后,所有 位于边上 的 不被 'X' 包围的 'O' 就全部变成了 '#' ,然后回到主函数中,再进行一次 二维矩阵 遍历,先将所有的 'O' 置为 'X' ,再将所有的 '#' 恢复为 'O' 。