写DFS函数的时候首先确定当前位置是否已经加入路径
DFS函数大概率会传递“位置信息”,根据位置信息获取下一步的选择,(大部分是在循环中)选择、执行、回退
在哪做选择,就在哪退出选择,参考题9
def DFS():
# 出口
if 满足出口条件:
将满足条件的路径加入结果
方法返回
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
DFS(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表
DFS和BFS本质上是决策树的遍历。
动态规划要求重叠子问题的条件,而DFS/BFS则是没有任何条件的纯暴力搜索。而且,BFS找到的路径一定是最短的,但代价通常比DFS要大得多。
DFS通常的问题形式是,找各种意义的路径:棋盘摆放、集合划分等等。
BFS通常的问题形式是,寻找(决策树上的)最短路径。
画出递归树,想想每个节点该怎么做
文章目录
1.DFS - N皇后问题
- 每个格子都面临选择:放或者不放皇后
- 注意一个优化点:如果某行放了皇后,则下次递归直接跳到下行
class Solution(object):
res = []
def solveNQueens(self, n):
"""
:type n: int
:rtype: List[List[str]]
"""
self.res = []
path = [["." for _ in range(n)] for _ in range(n)]
self.dfs(path, 0, 0, n)
return self.res
def dfs(self, path, x, y, queens_left):
n = len(path)
# 出口
if queens_left == 0:
path_strs = []
for i in range(n):
path_strs.append("".join(path[i]))
self.res.append(path_strs)
return
elif x > n - 1 or y > n - 1:
return
# 一般情况
# 选择1:放皇后
is_queen_available = True
for k in range(n): # 检查行和列
is_queen_available = False if path[x][k] == "Q" else is_queen_available
is_queen_available = False if path[k][y] == "Q" else is_queen_available
temp_x, temp_y = x - 1, y - 1 # 检查左上方和右上方
while (temp_x >= 0 and temp_y >= 0):
if path[temp_x][temp_y] == "Q":
is_queen_available = False
temp_x -= 1
temp_y -= 1
temp_x, temp_y = x - 1, y + 1
while (temp_x >= 0 and temp_y <= n - 1):
if path[temp_x][temp_y] == "Q":
is_queen_available = False
temp_x -= 1
temp_y += 1
if is_queen_available:
path[x][y] = "Q" # 做选择
queens_left -= 1
self.dfs(path, x + 1, 0, queens_left)
path[x][y] = "." # 撤销选择
queens_left += 1
# 选择2:不放皇后
if y == n - 1:
x_new = x + 1
y_new = 0
else:
x_new = x
y_new = y + 1
self.dfs(path, x_new, y_new, queens_left)
2.DFS - 子集(幂集)
- 每次递归都面临问题:是否将下标 idx 的数字加入结果集
class Solution {
List<List<Integer>> result;
List<Integer> path;
public List<List<Integer>> subsets(int[] nums) {
// DFS,但是不能搜索idx之前的位置
path = new LinkedList<>();
result = new LinkedList<>();
result.add(new LinkedList<>()); // 手动加入空集
dfs(nums, 0);
return result;
}
private void dfs(int[] nums, int idx) {
if (idx == nums.length) {
return;
}
// 将nums[idx]添加到结果
path.add(nums[idx]);
result.add(new LinkedList(path));
// 保持nums[idx]继续搜索
dfs(nums, idx + 1);
// 撤回nums[idx]继续搜索
path.remove(path.size() - 1);
dfs(nums, idx + 1);
}
}
3.DFS - 组合
- 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合
class Solution {
List<List<Integer>> result = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
dfs(n, k, 0, 1);
return result;
}
private void dfs(int n, int k, int count, int idx) {
// 递归出口
if (count == k) {
result.add(new LinkedList<>(path));
return;
}
if (idx > n) {
return;
}
// 加入当前位置
path.add(idx);
dfs(n, k, count + 1, idx + 1);
path.remove(path.size() - 1);
// 不加入当前位置
dfs(n, k, count, idx + 1);
}
}
4.DFS - 括号生成
- 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
- 有效括号组合需满足:左括号必须以正确的顺序闭合。
class Solution {
List<String> result = new LinkedList<>();
StringBuilder path = new StringBuilder();
public List<String> generateParenthesis(int n) {
dfs(n, 0, 0);
return result;
}
private void dfs(int n, int leftCount, int rightCount) {
// 递归出口
if (rightCount == n) {
result.add(path.toString());
return;
}
if (leftCount > rightCount) {
// 放入右括号
path.append(")");
dfs(n, leftCount, rightCount + 1);
path.deleteCharAt(path.length() - 1);
}
if (leftCount < n) {
// 放入左括号
path.append("(");
dfs(n, leftCount + 1, rightCount);
path.deleteCharAt(path.length() - 1);
}
}
}
5.BFS - 二叉树的最小深度
找好搜索的结束条件,当一个节点没有左右子节点时,到达叶节点。
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
// 层序遍历,如果某个节点没有叶子节点则返回
Deque<TreeNode> q = new LinkedList<>();
q.offerLast(root);
int depth = 0;
while (q.size() > 0) {
depth++;
int nodeNum = q.size();
for (int i = 0; i < nodeNum; i++) {
TreeNode node = q.pollFirst();
if (node.left == null && node.right == null) {
return depth;
}
if (node.left != null) {
q.offerLast(node.left);
}
if (node.right != null) {
q.offerLast(node.right);
}
}
}
return 0;
}
}
6.BFS - 转盘锁
class Solution(object):
def openLock(self, deadends, target):
"""
:type deadends: List[str]
:type target: str
:rtype: int
"""
# 工具方法
def plus(num, bit):
origin = int(num[bit])
if origin < 9:
origin += 1
else:
origin = 0
if bit == 0:
return str(origin) + num[1: ]
elif bit == 3:
return num[0: 3] + str(origin)
else:
return num[0: bit] + str(origin) + num[bit + 1: ]
def minus(num, bit):
origin = int(num[bit])
if origin > 0:
origin -= 1
else:
origin = 9
if bit == 0:
return str(origin) + num[1: ]
elif bit == 3:
return num[0: 3] + str(origin)
else:
return num[0: bit] + str(origin) + num[bit + 1: ]
# 特判
if "0000" in deadends:
return -1
# BFS
added = ["0000"] # 不走回头路,重要!
queue = ["0000"]
times = 0
while (len(queue) > 0):
length = len(queue)
for i in range(length):
code = queue[i]
# 出口
if code == target:
return times
# 处理每一位
for j in range(4):
temp_code = plus(code, j) # +1
if temp_code not in deadends and temp_code not in added:
queue.append(temp_code)
added.append(temp_code)
temp_code = minus(code, j) # -1
if temp_code not in deadends and temp_code not in added:
queue.append(temp_code)
added.append(temp_code)
times += 1
queue = queue[length:]
return -1
以上为经典的BFS解法。对于已知终点的BFS问题,可以采用双向BFS加速运行,从起点和终点向中间扩展,直到出现交集。
int openLock(String[] deadends, String target) {
Set<String> deads = new HashSet<>();
for (String s : deadends) deads.add(s);
// 用集合不用队列,可以快速判断元素是否存在
Set<String> q1 = new HashSet<>();
Set<String> q2 = new HashSet<>();
Set<String> visited = new HashSet<>();
int step = 0;
q1.add("0000");
q2.add(target);
while (!q1.isEmpty() && !q2.isEmpty()) {
// 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
Set<String> temp = new HashSet<>();
/* 将 q1 中的所有节点向周围扩散 */
for (String cur : q1) {
/* 判断是否到达终点 */
if (deads.contains(cur))
continue;
if (q2.contains(cur))
return step;
visited.add(cur);
/* 将一个节点的未遍历相邻节点加入集合 */
for (int j = 0; j < 4; j++) {
String up = plusOne(cur, j);
if (!visited.contains(up))
temp.add(up);
String down = minusOne(cur, j);
if (!visited.contains(down))
temp.add(down);
}
}
/* 在这里增加步数 */
step++;
// temp 相当于 q1
// 这里交换 q1 q2,下一轮 while 就是扩散 q2
q1 = q2;
q2 = temp;
}
return -1;
}
7.DFS - 组合总数
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
class Solution {
List<List<Integer>> result = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
dfs(candidates, target, 0, 0);
return result;
}
private void dfs(int[] candidates, int target, int sum, int idx) {
// 递归出口
if (idx >= candidates.length) {
return;
}
if (sum == target) {
result.add(new LinkedList<>(path));
return;
}
// 1.使用当前位置的值
if (candidates[idx] + sum <= target) {
path.add(candidates[idx]);
dfs(candidates, target, sum + candidates[idx], idx);
path.remove(path.size() - 1);
}
// 2.不使用当前位置的值
dfs(candidates, target, sum, idx + 1);
}
}
8.DFS - 字符串的全排列
class Solution {
Set<String> result = new HashSet<>();
StringBuilder path = new StringBuilder();
boolean[] visited;
public String[] permutation(String s) {
visited = new boolean[s.length()];
dfs(s, 0);
return result.toArray(new String[result.size()]);
}
private void dfs(String s, int count) {
int length = s.length();
if (count == length) {
result.add(path.toString());
return;
}
for (int i = 0; i < s.length(); i++) {
if (!visited[i]) {
visited[i] = true;
path.append(s.charAt(i));
dfs(s, count + 1);
visited[i] = false;
path.deleteCharAt(path.length() - 1);
}
}
}
}
9.DFS - 是否存在word路径
class Solution {
private char[] letters;
private int m;
private int n;
private boolean[][] visited;
private char[][] board;
public boolean exist(char[][] board, String word) {
m = board.length;
n = board[0].length;
letters = word.toCharArray();
this.board = board;
visited = new boolean[m][n];
// 初始化访问矩阵
for (int x = 0; x < m; x++) {
for (int y = 0; y < n; y++) {
visited[x][y] = false;
}
}
// 执行搜索
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (letters[0] == board[i][j]) {
if (dfs(i, j, 1)) {
return true;
}
}
}
}
return false;
}
private boolean dfs(int x, int y, int idx) {
// 递归出口
if (idx == letters.length) {
return true;
}
visited[x][y] = true; // DFS: 向前
// 向上下左右扩展
int lx = x;
int ly = y - 1;
boolean l = false;
int rx = x;
int ry = y + 1;
boolean r = false;
int ux = x - 1;
int uy = y;
boolean u = false;
int dx = x + 1;
int dy = y;
boolean d = false;
if (ly >= 0 && !visited[lx][ly] && board[lx][ly] == letters[idx]) {
l = dfs(lx, ly, idx + 1);
}
if (ry < n && !visited[rx][ry] && board[rx][ry] == letters[idx]) {
r = dfs(rx, ry, idx + 1);
}
if (ux >= 0 && !visited[ux][uy] && board[ux][uy] == letters[idx]) {
u = dfs(ux, uy, idx + 1);
}
if (dx < m && !visited[dx][dy] && board[dx][dy] == letters[idx]) {
d = dfs(dx, dy, idx + 1);
}
visited[x][y] = false; // DFS: 撤销
return l || r || u || d;
}
}
10.DFS + MEMO - 最长递增路径
给定一个 n 行 m 列矩阵 matrix ,矩阵内所有数均为非负整数。 你需要在矩阵中找到一条最长路径,使这条路径上的元素是递增的。并输出这条最长路径的长度
- 如果用纯DFS会超时
- 发现重叠子问题:记录以每个位置开始的最长路径长度,如果邻接的格子比当前更大,则一定没走过
class Solution {
int[][] dp; // 记录从dp[i][j]开始的最长路径长度
public int longestIncreasingPath(int[][] matrix) {
int result = 0;
int m = matrix.length;
int n = matrix[0].length;
dp = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
result = Math.max(dfs(matrix, i, j, m, n), result);
}
}
return result;
}
private int dfs(int[][] matrix, int x, int y, int m, int n) {
// 查表
if(dp[x][y] > 0) {
return dp[x][y];
}
// 求上下左右的最大长度
int u = 0;
int d = 0;
int l = 0;
int r = 0;
int ux = x - 1;
int uy = y;
int dx = x + 1;
int dy = y;
int lx = x;
int ly = y - 1;
int rx = x;
int ry = y + 1;
// 做选择
if (ux >= 0 && matrix[ux][uy] > matrix[x][y]) {
u = dfs(matrix, ux, uy, m, n);
}
if (dx < m && matrix[dx][dy] > matrix[x][y]) {
d = dfs(matrix, dx, dy, m, n);
}
if (ly >= 0 && matrix[lx][ly] > matrix[x][y]) {
l = dfs(matrix, lx, ly, m, n);
}
if (ry < n && matrix[rx][ry] > matrix[x][y]) {
r = dfs(matrix, rx, ry, m, n);
}
dp[x][y] = Arrays.stream(new int[]{u, d, l, r}).max().getAsInt() + 1;
return dp[x][y];
}
}
11.DFS - 循环依赖
class P3:
def __init__(self):
self.res = []
self.path = []
def find(self, serivces):
num = len(serivces)
visited = [False for _ in range(num)]
def dfs(idx, num):
# idx已经加入!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
(n0, n1) = serivces[idx]
# 检查是否有循环依赖
for t in self.path[: -1]: # 因为最后一个元素是新加入的所以不能用来检测成环!!!!!!!!!!!!!!!!!!!!
if t[0] == n1 or t[1] == n1:
self.res.append(self.path[:])
return None
# 继续进行
for i in range(num):
if not visited[i] and serivces[i][0] == n1:
self.path.append(serivces[i])
visited[i] = True
dfs(i, num)
self.path.pop()
visited[i] = False
for i in range(num):
self.path.append(serivces[i])
visited[i] = True
dfs(i, num)
self.path.pop()
visited[i] = False
return self.res
print(P3().find([('A', 'B'), ('B', 'C'), ('C', 'A'), ('A', 'D')]))
12.DFS + MEMO - 跳跃游戏
- DFS + MEMO
- 使用 Set 记录无法到达的位置,避免重复搜索
class Solution {
Set<Integer> notAvailable = new HashSet<>(); // 记录不可达的位置,避免重复搜索
public boolean canJump(int[] nums) {
return dfs(nums, 0);
}
private boolean dfs(int[] nums, int idx) {
if (idx >= nums.length - 1) {
return true;
}
if (notAvailable.contains(idx)) {
return false;
}
for (int i = 1; i <= nums[idx]; i++) {
if (dfs(nums, idx + i)) {
return true;
}
}
notAvailable.add(idx);
return false;
}
}
- 更巧妙的贪心法
class Solution {
public boolean canJump(int[] nums) {
// 贪心法
int mostRightIdx = 0;
for (int i = 0; i < nums.length; i++) {
if (mostRightIdx < i) {
return false;
}
mostRightIdx = Math.max(mostRightIdx, nums[i] + i);
}
return (mostRightIdx >= nums.length - 1);
}
}