回溯(Backtracking)和深度优先搜索(DFS, Depth-First Search)是两种在图和树结构中常见的搜索策略。尽管它们在实现上有很多相似之处,但它们的应用场景和解题思路有所不同。
1. DFS(深度优先搜索):
DFS 是一种在图中遍历节点的策略,目标是尽可能深入地探索节点的子节点,直到访问到没有未访问的邻居为止。DFS 通常用递归或栈来实现。
特点:
-
遍历方式:每次沿着一条路径向下深入,直到该路径无法继续为止。
-
递归或栈:通常使用递归函数实现,或者显式地使用栈来模拟递归。
-
回溯条件:当走到路径的尽头时,回退到上一个节点,再尝试另一条路径。
DFS 用法:
-
遍历树或图,找到所有路径、连接或答案。
-
适用于所有需要在图中逐个节点查找解的问题,特别是在树结构或者无环图中。
代码示例:图的深度优先搜索
import java.util.*;
public class DFSExample {
public static void dfs(int[][] graph, int node, boolean[] visited) {
// 访问当前节点
visited[node] = true;
System.out.print(node + " ");
// 递归访问所有邻居
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
dfs(graph, neighbor, visited);
}
}
}
public static void main(String[] args) {
// 示例图(邻接表表示)
int[][] graph = {
{1, 2}, // 节点0的邻居是节点1和2
{0, 3, 4}, // 节点1的邻居是节点0、3、4
{0, 4}, // 节点2的邻居是节点0和4
{1}, // 节点3的邻居是节点1
{1, 2} // 节点4的邻居是节点1和2
};
boolean[] visited = new boolean[graph.length];
dfs(graph, 0, visited); // 从节点0开始深度优先搜索
}
}
解释:
-
这个例子中,我们定义了一个图,使用邻接表表示。
-
dfs
函数递归地访问图的节点,从当前节点出发访问所有未访问的邻居节点,直到遍历完所有可达节点。
2. 回溯(Backtracking):
回溯是一种递归算法,用于探索所有可能的解,并通过“撤销决策”来尝试不同的选择。回溯通常用于组合、排列、子集等问题,它尝试所有可能的路径,并在发现某条路径无效时回溯,返回到上一步重新选择。
特点:
-
递归 + 剪枝:回溯和 DFS 很相似,都采用递归来遍历所有可能的解,但回溯在递归过程中添加了剪枝操作,避免不必要的路径探索。
-
状态回退:在探索一个选择后,回溯会撤销当前选择,返回到之前的状态并尝试其他选择。
-
解空间树:回溯通常被用于枚举解空间中的所有可能解,并逐步剪除不符合条件的分支。
回溯常见问题:
-
组合问题:从一组数中选取多个数,可能带有某些约束条件。
-
排列问题:生成某个集合的所有排列。
-
子集问题:求某个集合的所有子集。
-
拼图问题:比如数独、N皇后等。
代码示例:N皇后问题
import java.util.*;
public class NQueens {
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
boolean[] cols = new boolean[n]; // 列
boolean[] diag1 = new boolean[2 * n]; // 主对角线
boolean[] diag2 = new boolean[2 * n]; // 副对角线
List<String> board = new ArrayList<>();
backtrack(result, board, n, 0, cols, diag1, diag2);
return result;
}
private void backtrack(List<List<String>> result, List<String> board, int n, int row,
boolean[] cols, boolean[] diag1, boolean[] diag2) {
if (row == n) {
result.add(new ArrayList<>(board)); // 结果符合要求,保存
return;
}
for (int col = 0; col < n; col++) {
if (cols[col] || diag1[row - col + n] || diag2[row + col]) {
continue; // 该列或对角线已经有皇后,跳过
}
// 做选择
cols[col] = diag1[row - col + n] = diag2[row + col] = true;
char[] rowChars = new char[n];
Arrays.fill(rowChars, '.');
rowChars[col] = 'Q';
board.add(new String(rowChars));
// 递归
backtrack(result, board, n, row + 1, cols, diag1, diag2);
// 撤销选择
board.remove(board.size() - 1);
cols[col] = diag1[row - col + n] = diag2[row + col] = false;
}
}
public static void main(String[] args) {
NQueens solution = new NQueens();
List<List<String>> result = solution.solveNQueens(4);
System.out.println(result);
}
}
解释:
-
在这个例子中,我们求解 N 皇后问题,即在 N x N 的棋盘上放置 N 个皇后,使得它们互不攻击。
-
使用回溯的方法,我们依次尝试在每一行放置皇后,并且通过
cols
、diag1
和diag2
数组检查是否有冲突。 -
每次尝试放置一个皇后后,递归进入下一行,直到所有皇后都放置完成。
回溯与DFS的区别:
-
回溯 是一种带有剪枝的深度优先搜索。它不是仅仅深度优先地去探索图或树的每一条路径,而是在路径无法继续时撤销当前选择,返回到上一个状态,尝试其他可能的路径。
-
DFS 是一种遍历图或树的技术,通常不涉及回溯和剪枝。DFS 会沿着某一条路径一直走到底,直到没有更多的未访问节点。
回溯和DFS的联系:
-
回溯是一种深度优先搜索(DFS)的方法。不同的是,回溯更关注“状态的恢复”或者“撤销”,而 DFS 更注重“沿路径遍历”的过程。
-
DFS 更适用于需要遍历每一个节点的情境,而 回溯 则更多用于那些寻找满足条件的解并进行剪枝的场景。
总结:
-
DFS 是一种纯粹的遍历方法,适用于不需要剪枝的图遍历。
-
回溯 是 DFS 的一种扩展,通常会在遍历过程中剪枝,避免重复的或者不可能成功的路径。回溯的重点在于“尝试-撤销”操作。
-
两者常常在处理组合、排列、路径问题时一起使用。