LeetCode LCR 110. 所有可能的路径
题目描述



题意分析
给定有向无环图,返回从节点
0到节点n-1的全部路径,输出顺序不限。邻接表只表示给定方向的边,不能反向行走。题目要求具体路径,因此到达同一节点的不同前缀都要保留。可以用 DFS 逐条枚举出边,并维护当前从起点走到的位置序列。
解法:DFS 回溯有向无环图路径
核心思路
[!blue]
用共享列表
path保存当前路径,第一次调用前先放入起点0。进入dfs(i)时,path恰好是从起点到i的完整路径;函数负责枚举从这里继续到目标的全部走法,返回时保持这条路径不变。若
i == n-1,当前路径已是一个答案,复制后保存并返回。停止的依据是到达指定目标,不需要假定目标一定没有出边,也不能继续走出目标后再把更长路径当成答案。否则,依次遍历每条出边
i→j:先把j加入路径,再调用dfs(j),返回后弹出最后一个节点。子调用会把自身继续探索的部分恢复,调用方再撤销这一次选择,下一条出边就仍从相同前缀出发。路径在探索过程中反复修改,因此保存答案时必须复制。Java 用新的
ArrayList保存当前元素;Go 将路径元素追加到新切片,使结果不再共享会被后续回溯修改的底层数组。有向无环保证一条路径不会再次回到已经包含的节点,递归深度不会超过节点数,无需额外判环。不同路径可以经过同一个节点,所以不能用不回退的全局访问标记屏蔽后来的前缀。每条出边都被枚举,每个选择序列只探索一次,得到的正是全部目标路径;提前走到没有出边的非目标节点时,则直接返回,不保存答案。
解题步骤
- 初始化空答案列表和只包含起点的当前路径,再调用
dfs(0)。- 当前节点为目标时,保存路径副本并立即返回。
- 否则遍历当前节点的每个出边邻居,执行加入邻居、递归、弹出邻居。
- 当前节点没有出边或所有分支处理完后返回,路径恢复为进入本次调用时的状态。
- 顶层调用结束后返回答案;没有任何路径到达目标时,结果保持为空。
代码实现
class Solution {
private List<List<Integer>> answer;
private int[][] graph;
public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
answer = new ArrayList<>();
this.graph = graph;
List<Integer> path = new ArrayList<>();
// 进入 dfs(i) 时 path 末尾必须是 i,起点也不例外。
path.add(0);
dfs(0, path);
return answer;
}
private void dfs(int i, List<Integer> path) {
if (i == graph.length - 1) {
// path 是共享可变对象,必须复制一份存入结果。
answer.add(new ArrayList<>(path));
return;
}
// 图无环,任何出边都不会走回已在 path 中的节点,无需 visited。
for (int j : graph[i]) {
path.add(j);
dfs(j, path);
// 递归结束后撤销选择,让兄弟分支从干净状态出发。
path.remove(path.size() - 1);
}
}
}
func allPathsSourceTarget(graph [][]int) [][]int {
var path []int
// 进入 dfs(i) 时 path 末尾必须是 i,起点也不例外。
path = append(path, 0)
var answer [][]int
var dfs func(i int)
dfs = func(i int) {
if i == len(graph)-1 {
// path 底层数组会被后续回溯复用,必须复制一份存入结果。
answer = append(answer, append([]int(nil), path...))
return
}
// 图无环,任何出边都不会走回已在 path 中的节点,无需 visited。
for _, j := range graph[i] {
path = append(path, j)
dfs(j)
// 递归结束后撤销选择,让兄弟分支从干净状态出发。
path = path[:len(path)-1]
}
}
dfs(0)
return answer
}
复杂度分析
- 时间复杂度:最坏为 $O(n2^n)$。DAG 中不同路径前缀本身可能有指数数量,枚举和复制路径需要计入路径长度;搜索还可能进入不能到达目标的分支,不能只按最终成功输出的条数计时。
- 空间复杂度:当前路径和递归栈的辅助空间为 $O(n)$。结果还需要保存所有输出路径,按实际输出节点总数另外计算。
关键点总结
[!green]
- 进入递归时路径已经包含当前节点,起点也遵守同一个约定。
- 选择、递归、撤销保持成对,使不同分支共享存储而不互相污染。
- 当前路径可复用,已经收集的答案必须有独立副本。
- 无环限制单条路径深度,全局访问标记却会误删不同前缀形成的其他答案。
易错点总结
[!yellow]
- 只记录访问过的节点而不记录路径前缀,会把不同答案合并掉。
- 递归返回后不弹出末项,下一分支会带上前一分支的残留节点。
- 保存共享列表或切片引用,后续回溯会改动已经收集的结果。
- 初始路径没有起点,最终每条答案都会少一个节点。
- 只在无出边节点处收集答案,会把非目标死路算进去;应判断当前编号是否为
n-1。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 126. 单词接龙 II | 困难 | 同样输出从起点到终点的多条路径,原题只要最短路径,本题DAG中所有路径都要保留。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!