算法刷题系列(二十九):DFS——沿路径深入并维护访问状态
算法刷题系列(二十九):DFS——沿路径深入并维护访问状态
算法刷题系列(二十九):DFS——沿路径深入并维护访问状态
深度优先搜索(DFS)沿一条分支尽可能深入,再回退尝试其他分支。树的结构没有环,递归访问左右孩子即可;一般图可能有环或多条路径到达同一节点,必须区分“当前路径上的节点”和“已经全局处理过的节点”。
一、树路径与图访问的差别
在树里求是否存在根到叶的目标和,路径状态可通过递归参数传递。离开递归调用时,局部变量随栈帧返回,不需要全局 visited。
在图 0—1—2—0 中,若沿邻接边递归却不标记访问状态,搜索会无限绕圈。若目标是判断可达性或遍历连通分量,全局访问集合通常应在进入节点时标记。若任务是枚举所有简单路径,则节点可在回溯时撤销“当前路径访问”标记,以允许它出现在另一条候选路径中;这与全局去重不是同一个语义。
二、树:根到叶路径和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Main {
static final class Node {
final int value;
Node left;
Node right;
Node(int value) { this.value = value; }
}
static boolean hasPathSum(Node node, long remaining) {
if (node == null) return false;
long afterNode = remaining - node.value;
if (node.left == null && node.right == null) {
return afterNode == 0;
}
return hasPathSum(node.left, afterNode)
|| hasPathSum(node.right, afterNode);
}
public static void main(String[] args) {
Node root = new Node(5);
root.left = new Node(4);
root.right = new Node(8);
root.left.left = new Node(11);
root.left.left.left = new Node(7);
root.left.left.right = new Node(2);
root.right.right = new Node(4);
assert hasPathSum(root, 22);
assert !hasPathSum(root, 18);
assert !hasPathSum(null, 0);
}
}
remaining 表示从当前节点开始还需要凑出的和;只有到达叶子且恰好归零才成功。节点值可以为负数,因此不能仅因当前和超过目标就剪枝。
三、图:可达节点 DFS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def reachable_count(graph: list[list[int]], start: int) -> int:
if not 0 <= start < len(graph):
raise ValueError("start vertex is out of range")
visited = {start}
stack = [start]
while stack:
vertex = stack.pop()
for neighbor in graph[vertex]:
if not 0 <= neighbor < len(graph):
raise ValueError("neighbor vertex is out of range")
if neighbor not in visited:
visited.add(neighbor)
stack.append(neighbor)
return len(visited)
graph = [[1, 2], [0, 2], [0, 1, 3], [2], []]
assert reachable_count(graph, 0) == 4
assert reachable_count(graph, 4) == 1
四、Java 图实现:迭代栈与状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class Main {
static int reachableCount(List<List<Integer>> graph, int start) {
int n = graph.size();
if (start < 0 || start >= n) {
throw new IllegalArgumentException("start vertex is out of range");
}
boolean[] visited = new boolean[n];
ArrayDeque<Integer> stack = new ArrayDeque<>();
visited[start] = true;
stack.push(start);
int count = 0;
while (!stack.isEmpty()) {
int vertex = stack.pop();
count++;
for (int neighbor : graph.get(vertex)) {
if (neighbor < 0 || neighbor >= n) {
throw new IllegalArgumentException("neighbor is out of range");
}
if (!visited[neighbor]) {
visited[neighbor] = true;
stack.push(neighbor);
}
}
}
return count;
}
public static void main(String[] args) {
List<List<Integer>> graph = new ArrayList<>();
graph.add(List.of(1, 2));
graph.add(List.of(0, 2));
graph.add(List.of(0, 1, 3));
graph.add(List.of(2));
graph.add(List.of());
assert reachableCount(graph, 0) == 4;
assert reachableCount(graph, 4) == 1;
}
}
五、正确性、复杂度与误区
图 DFS 的不变式是:已访问集合中的每个节点都从起点可达;每次仅把尚未标记的邻居压栈,并在压栈时立即标记,从而保证每个顶点最多入栈一次。邻接表实现的时间复杂度为 O(V + E),空间复杂度为 O(V)。有向图只沿出边访问;若要找弱连通分量,需按问题定义将边视为无向或构建反向关系。
- 树的递归搜索不要机械地添加全局 visited,除非输入可能共享节点或不是树。
- 图搜索不要只靠递归深度限制来避免环;需要正确的 visited 语义。
- 枚举所有简单路径时,结果数可能是指数级,不能把复杂度简单说成
O(V+E)。 - 若只找一条可达路径,可额外记录父节点;若需最少边数,下一篇的 BFS 更合适。
小结与衔接
DFS 的本质是沿分支深入,访问状态的生命周期取决于问题:遍历图时全局保留,枚举路径时可能在回溯中撤销,树递归中通常不需要。需要无权图最短路径时,下一篇将用 BFS 按层扩展,首次发现节点就能确定最少边数。
本文由作者按照 CC BY 4.0 进行授权