代码随想录算法day43 | 图论part01 | 图论理论基础,深搜理论基础,98. 所有可达路径,广搜理论基础

大家可以在看图论理论基础的时候,很多内容 看不懂,例如也不知道 看完之后 还是不知道 邻接矩阵,邻接表怎么用, 别着急。
理论基础大家先对各个概念有个印象就好,后面在刷题的过程中,每个知识点都会得到巩固。

图论理论基础

图的基本概念

二维坐标中,两点可以连成线,多个点连成的线就构成了图。

当然图也可以就一个节点,甚至没有节点(空图)

图的种类

整体上一般分为 有向图无向图

有向图 是指图中边是有方向的:

无向图 是指图中边没有方向:

加权有向图 就是图中边是有权值的,例如:

加权无向图也是同理。

无向图中有几条边连接该节点,该节点就有几度。

例如,该无向图中,节点4的度为5,节点6的度为3。

在有向图中,每个节点有出度和入度。

出度:从该节点出发的边的个数。

入度:指向该节点边的个数。

例如,该有向图中,节点3的入度为2,出度为1,节点1的入度为0,出度为2。

连通性

在图中表示节点的连通情况,我们称之为连通性。

连通图

在无向图中,任何两个节点都是可以到达的,我们称之为连通图 ,如图:

如果有节点不能到达其他节点,则为非连通图,如图:

节点1 不能到达节点4。

强连通图

在有向图中,任何两个节点是可以相互到达的,我们称之为 强连通图。

这里有人可能想,这和无向图中的连通图有什么区别,不是一样的吗?

我们来看这个有向图:

这个图是强连通图吗?

初步一看,好像这节点都连着呢,但这不是强连通图,节点1 可以到节点5,但节点5 不能到 节点1 。

强连通图是在有向图中任何两个节点是可以相互到达

下面这个有向图才是强连通图:

连通分量

在无向图中的极大连通子图称之为该图的一个连通分量。

只看概念大家可能不理解,我来画个图:

该无向图中 节点1、节点2、节点5 构成的子图就是该无向图中的一个连通分量,该子图所有节点都是相互可达到的。

同理,节点3、节点4、节点6 构成的子图也是该无向图中的一个连通分量。

那么无向图中 节点3 、节点4 构成的子图是该无向图的联通分量吗?

不是!

因为必须是极大联通子图才能是连通分量,所以必须是 节点3、节点4、节点6 构成的子图才是连通分量。

在图论中,连通分量是一个很重要的概念,例如岛屿问题其实就是求连通分量。

强连通分量

在有向图中极大强连通子图称之为该图的强连通分量。

如图:

节点1、节点2、节点3、节点4、节点5 构成的子图是强连通分量,因为这是强连通图,也是极大图。

节点6、节点7、节点8 构成的子图不是强连通分量,因为这不是强连通图,节点8 不能达到节点6。

节点1、节点2、节点5 构成的子图 也不是 强连通分量,因为这不是极大图。

图的构造

我们如何用代码来表示一个图呢?

一般使用邻接表、邻接矩阵 或者用类来表示。

主要是 朴素存储、邻接表和邻接矩阵。

邻接矩阵

使用二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。

例如: grid[2][5] = 6,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5,边的权值为6。

如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。

如图:

在一个 n (节点数)为 8 的图中,就需要申请 8 * 8 这么大的空间。

图中有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6

这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。

而且在寻找节点连接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。

邻接矩阵的优点:

  • 表达方式简单,易于理解
  • 检查任意两个顶点间是否存在边的操作非常快
  • 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。

缺点:

  • 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历边的时候需要遍历整个n * n矩阵,造成时间浪费

邻接表

使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边才会申请对应大小的链表。

邻接表的构造如图:

这里表达的图是:

  • 节点1 指向 节点3 和 节点5
  • 节点2 指向 节点4、节点3、节点5
  • 节点3 指向 节点4
  • 节点4指向节点1

有多少边,邻接表才会申请多少个对应的链表节点。

从图中可以直观看出 使用 数组 + 链表 来表达 边的连接情况 。

邻接表的优点:

  • 对于稀疏图的存储,只需要存储边,空间利用率高
  • 遍历节点连接情况相对容易

缺点:

  • 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点连接其他节点的数量。
  • 实现相对复杂,不易理解

以上大家可能理解比较模糊,没关系,因为大家还没做过图论的题目,对于图的表达没有概念。大家先有个初步印象,在后面算法题实战中,还会讲到具体代码实现,写了代码之后,自然就理解了。

图的遍历方式

图的遍历方式基本是两大类:

  • 深度优先搜索(dfs)
  • 广度优先搜索(bfs)

在讲解二叉树章节的时候,其实就已经讲过这两种遍历方式。

二叉树的递归遍历,是dfs 在二叉树上的遍历方式。

二叉树的层序遍历,是bfs 在二叉树上的遍历方式。

dfs 和 bfs 可以在不同的数据结构上进行搜索,在二叉树章节里是在二叉树这样的数据结构上搜索。

而在图论章节,则是在图(邻接表或邻接矩阵)上进行搜索。

总结

以上知识点 大家先有个印象,图论是非常庞大的知识体系,上面的内容还不足以概括图论内容,仅仅是理论基础而已。

在图论章节会深入讲解 深度优先搜索(DFS)、广度优先搜索(BFS)、并查集、拓扑排序、最小生成树系列、最短路算法系列等等。


深度优先搜索理论基础

dfs 与 bfs 区别

提到深度优先搜索(dfs),就不得不说和广度优先搜索(bfs)有什么区别

先给大家说一下两者大概的区别:

  • dfs是坚持一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
  • bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。

当然以上讲的是,大体可以这么理解,接下来 我们先详细讲解dfs

dfs 搜索过程

上面说道dfs是坚持一个方向搜,不到黄河不回头。 那么我们来举一个例子。

如图一,是一个无向图,我们要搜索从节点1到节点6的所有路径。

那么dfs搜索的第一条路径是这样的: (假设第一次延默认方向,就找到了节点6),图二

此时我们找到了节点6,(遇到黄河了,是不是应该回头了),那么应该再去搜索其他方向了。 如图三:

路径2撤销了,改变了方向,走路径3(红色线), 接着也找到终点6。 那么撤销路径2,改为路径3,在dfs中其实就是回溯的过程。

又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图四中,路径4撤销(回溯的过程),改为路径5。

又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图五,路径6撤销(回溯的过程),改为路径7,路径8 和 路径7,路径9, 结果发现死路一条,都走到了自己走过的节点。

那么节点2所连接路径和节点3所链接的路径 都走过了,撤销路径只能向上回退,去选择撤销当初节点4的选择,也就是撤销路径5,改为路径10 。 如图图六:

上图演示中,其实我并没有把 所有的 从节点1 到节点6的dfs(深度优先搜索)的过程都画出来,那样太冗余了,但 已经把dfs 关键的地方都涉及到了,关键就两点:

  • 搜索方向,是认准一个方向搜,直到碰壁之后再换方向
  • 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。

代码框架

正是因为dfs搜索坚持一个方向,并需要回溯,所以用递归的方式来实现是最方便的。

有递归的地方就有回溯,那么回溯在哪里呢?

就递归函数的下面,例如如下代码:

java 复制代码
void dfs(参数) {
    处理节点
    dfs(图,选择的节点); // 递归
    回溯,撤销处理结果
}

可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的。

在讲解二叉树的时候,二叉树的递归法其实就是dfs,而二叉树的迭代法,就是bfs(广度优先搜索)

所以dfs,bfs其实是基础搜索算法,也广泛应用与其他数据结构与算法中

我们在回顾一下 回溯 的代码框架:

java 复制代码
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

回溯算法,其实就是dfs的过程,这里给出dfs的代码框架:

java 复制代码
void dfs(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本节点所连接的其他节点) {
        处理节点;
        dfs(图,选择的节点); // 递归
        回溯,撤销处理结果
    }
}

可以发现dfs的代码框架和回溯算法的代码框架是差不多的。

下面我在用 深搜三部曲,来解读 dfs的代码框架。

深搜三部曲

在 二叉树递归讲解 中,给出了递归三部曲。

回溯算法讲解 中,给出了回溯三部曲。

其实深搜也是一样的,深搜三部曲如下:

  • 确认递归函数,参数
java 复制代码
public void dfs(参数)

通常我们递归的时候,我们递归搜索需要了解哪些参数,其实也可以在写递归函数的时候,发现需要什么参数,再去补充就可以。

一般情况,深搜需要 二维数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。

例如这样:

java 复制代码
int[][] result; // 保存符合条件的所有路径
int[] path; // 起点到终点的路径
public void dfs (图,目前搜索的节点)  

但这种写法看个人习惯,不强求。

  • 确认终止条件

终止条件很重要,很多同学写dfs的时候,之所以容易死循环,栈溢出等等这些问题,都是因为终止条件没有想清楚。

java 复制代码
if (终止条件) {
    存放结果;
    return;
}

终止添加不仅是结束本层递归,同时也是我们收获结果的时候。

另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。这里如果大家不理解的话,没关系,后面会有具体题目来讲解。

  • 处理目前搜索节点出发的路径

一般这里就是一个for循环的操作,去遍历 目前搜索节点 所能到的所有节点。

java 复制代码
for (选择:本节点所连接的其他节点) {
    处理节点;
    dfs(图,选择的节点); // 递归
    回溯,撤销处理结果
}

不少朋友疑惑的地方,都是 dfs代码框架中for循环里分明已经处理节点了,那么 dfs函数下面 为什么还要撤销的呢。

如图七所示, 路径2 已经走到了 目的地节点6,那么 路径2 是如何撤销,然后改为 路径3呢? 其实这就是 回溯的过程,撤销路径2,走换下一个方向。


广度优先搜索理论基础

在dfs的讲解中,我们就讲过深度优先搜索和广度优先搜索的区别。

广搜(bfs)是一圈一圈的搜索过程,和深搜(dfs)是一条路跑到黑然后再回溯。

广搜的使用场景

广搜的搜索方式就适合于解决两个点之间的最短路径问题。

因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。

当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行。 (我们会在具体题目讲解中详细来说)

广搜的过程

上面我们提过,BFS是一圈一圈的搜索过程,但具体是怎么一圈一圈来搜呢。

我们用一个方格地图,假如每次搜索的方向为 上下左右(不包含斜上方),那么给出一个start起始位置,那么BFS就是从四个方向走出第一步。

如果加上一个end终止位置,那么使用BFS的搜索过程如图所示:

我们从图中可以看出,从start起点开始,是一圈一圈,向外搜索,方格编号1为第一步遍历的节点,方格编号2为第二步遍历的节点,第四步的时候我们找到终止点end。

正是因为BFS一圈一圈的遍历方式,所以一旦遇到终止点,那么一定是一条最短路径。

而且地图还可以有障碍,如图所示:

在第五步,第六步 我只把关键的节点染色了,其他方向周边没有去染色,大家只要关注关键地方染色的逻辑就可以。

从图中可以看出,如果添加了障碍,我们是第六步才能走到end终点。

只要BFS只要搜到终点一定是一条最短路径,大家可以参考上面的图,自己再去模拟一下。

代码框架

大家应该好奇,这一圈一圈的搜索过程是怎么做到的,是放在什么容器里,才能这样去遍历。

很多网上的资料都是直接说用队列来实现。

其实,我们仅仅需要一个容器 ,能保存我们要遍历过的元素就可以,那么用队列,还是用栈,甚至用数组,都是可以的

用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针

因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。

如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历

因为栈是先进后出,加入元素和弹出元素的顺序改变了。

那么广搜需要注意转圈搜索的顺序吗? 不需要!

所以用队列,还是用栈都是可以的,但大家都习惯用队列了,所以下面的讲解用我也用队列来讲,只不过要给大家说清楚,并不是非要用队列,用栈也可以

下面给出广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释)

java 复制代码
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(char[][] grid, bool[][] visited, int x, int y) {
    Queue<int, int> que = new LinkedList<>(); // 定义队列
    que.push({x, y}); // 起始节点加入队列
    visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
    while(!que.empty()) { // 开始遍历队列里的元素
        pair<int ,int> cur = que.front();
        que.pop(); // 从队列取元素
        int curx = cur.first;
        int cury = cur.second; // 当前节点坐标
        for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
            int nextx = curx + dir[i][0];
            int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
            if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;  // 坐标越界了,直接跳过
            if (!visited[nextx][nexty]) { // 如果节点没被访问过
                que.push({nextx, nexty});  // 队列添加该节点为下一轮要遍历的节点
                visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
            }
        }
    }

}

总结

当然广搜还有很多细节需要注意的地方,后面会针对广搜的题目还做针对性的讲解。

因为在理论篇讲太多细节,可能会让刚学广搜的朋友越看越懵,所以细节方面针对具体题目在做讲解。

本篇我们重点讲解了广搜的使用场景,广搜的过程以及广搜的代码框架。

其实在二叉树章节的层次遍历中,我们也讲过一次广搜,相当于是广搜在二叉树这种数据结构上的应用。

这次则从图论的角度上再详细讲解一次广度优先遍历。


98. 所有可达路径

卡码网题目链接(ACM模式)(opens new window)

【题目描述】

给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。

【输入描述】

第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边

后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径

【输出描述】

输出所有的可达路径,路径中所有节点的后面跟一个空格,每条路径独占一行,存在多条路径,路径输出的顺序可任意。

如果不存在任何一条路径,则输出 -1。

注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 1 3 5,而不是 1 3 5, 5后面没有空格!

【输入示例】

5 5
1 3
3 5
1 2
2 4
4 5

【输出示例】

1 3 5
1 2 4 5  

提示信息

用例解释:

有五个节点,其中的从 1 到达 5 的路径有两个,分别是 1 -> 3 -> 5 和 1 -> 2 -> 4 -> 5。

因为拥有多条路径,所以输出结果为:

1 3 5
1 2 4 5

1 2 4 5
1 3 5

都算正确。

数据范围:

  • 图中不存在自环
  • 图中不存在平行边
  • 1 <= N <= 100
  • 1 <= M <= 500

力扣是核心代码模式,把图的存储方式给大家定义好了,只需要写出深搜的核心代码就可以。

如果笔试的时候出一道原题 (笔试都是ACM模式,部分面试也是ACM模式),不少熟练刷力扣的朋友都难住了,因为不知道图应该怎么存,也不知道自己存的图如何去遍历。

所以这也是为什么我要让大家练习 ACM模式,也是我为什么 在代码随想录图论讲解中,不惜自己亲自出题,让大家统一练习ACM模式。

这道题目是深度优先搜索(bfs),比较好的入门题。

那有的朋友可能说:那我以后称回溯算法为深搜,是不是没毛病?

理论上来说,没毛病,但 就像是 二叉树 你不叫它二叉树,叫它数据结构,有问题不? 也没问题对吧。

建议是 有细分的场景,还是称其细分场景的名称。 所以回溯算法可以独立出来,但回溯确实就是深搜。

图的存储

在图论理论基础篇中我们讲到了 两种 图的存储方式:邻接表 和 邻接矩阵。

本题我们将带大家分别实现这两个图的存储方式。

邻接矩阵

邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。

本题我们会有 n 个节点,因为节点标号是从1开始的,为了节点标号和下标对齐,我们申请 n + 1 * n + 1 这么大的二维数组。

java 复制代码
int[][] graph = new int[n + 1][n + 1];

输入m个边,构造方式如下:

java 复制代码
for (int i = 0; i < m; i++) {
    int s = scanner.nextInt();
    int t = scanner.nextInt();
    // 使用邻接矩阵表示无向图,1 表示 s 与 t 是相连的
    graph[s][t] = 1;
}

邻接表

邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。

邻接表的构造相对邻接矩阵难理解一些。

图理论基础篇 举了一个例子:

这里表达的图是:

  • 节点1 指向 节点3 和 节点5
  • 节点2 指向 节点4、节点3、节点5
  • 节点3 指向 节点4
  • 节点4指向节点1

我们需要构造一个数组,数组里的元素是一个链表。

Java写法:

java 复制代码
// 节点编号从1到n,所以申请 n+1 这么大的数组
List<LinkedList<Integer>> graph = new ArrayList<>(n + 1);

输入m个边,构造方式如下:

java 复制代码
while (m-- > 0) {
    int s = scanner.nextInt();
    int t = scanner.nextInt();
    // 使用邻接表表示 s -> t 是相连的
    graph.get(s).add(t);
}

本题我们使用邻接表 或者 邻接矩阵都可以,因为后台数据并没有对图的大小以及稠密度做很大的区分。

下面展示的是邻接表的代码写法

注意邻接表 和 邻接矩阵的写法都要掌握

深度优先搜索

本题是深度优先搜索的基础题目,关于深搜我在图论深搜理论基础 已经有详细的讲解,图文并茂。

关于本题我会直接使用深搜三部曲来分析,如果对深搜不够了解,建议先看 图论深搜理论基础

深搜三部曲来分析题目:

  • 确认递归函数,参数

首先我们dfs函数一定要存一个图,用来遍历的,需要存一个目前我们遍历的节点,定义为 x。

还需要存一个 n,表示终点,我们遍历的时候,用来判断当 x==n 时候标明找到了终点。

(其实在递归函数的参数 不容易一开始就确定了,一般是在写函数体的时候发现缺什么,参加就补什么)

至于 单一路径 和 路径集合 可以放在全局变量,那么代码是这样的:

java 复制代码
static List<List<Integer>> result = new ArrayList<>(); // 收集符合条件的路径
static List<Integer> path = new ArrayList<>(); // 1节点到终点的路径
// x:目前遍历的节点
// graph:存当前的图
// n:终点
public static void dfs(int[][] graph, int x, int n) {
  • 确认终止条件

什么时候我们就找到一条路径了?

当目前遍历的节点 为 最后一个节点 n 的时候 就找到了一条 从出发点到终止点的路径。

java 复制代码
// 当前遍历的节点x 到达节点n 
if (x == n) { // 找到符合条件的一条路径
    result.push_back(path);
    return;
}
  • 处理目前搜索节点出发的路径

接下来是走 当前遍历节点x的下一个节点。

首先是要找到 x节点指向了哪些节点呢? 遍历方式是这样的:

java 复制代码
for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点
    if (graph[x][i] == 1) { // 找到 x指向的节点,就是节点i
    }
}

接下来就是将 选中的x所指向的节点,加入到 单一路径来。

java 复制代码
path.add(i); // 遍历到的节点加入到路径中来

进入下一层递归

java 复制代码
dfs(graph, i, n); // 进入下一层递归

最后就是回溯的过程,撤销本次添加节点的操作。

该过程整体代码:

java 复制代码
for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点
    if (graph[x][i] == 1) { // 找到 x链接的节点
        path.add(i); // 遍历到的节点加入到路径中来
        dfs(graph, i, n); // 进入下一层递归
        path.remove(path.size() - 1); // 回溯,撤销本节点
    }
}

打印结果

ACM格式大家在输出结果的时候,要关注看看格式问题,特别是字符串,有的题目说的是每个元素后面都有空格,有的题目说的是 每个元素间有空格,最后一个元素没有空格。

有的题目呢,压根没说,那只能提交去试一试了。

很多录友在提交题目的时候发现结果一样,为什么提交就是不对呢。

例如示例输出是:

1 3 5 而不是 1 3 5

即 5 的后面没有空格!

这是我们在输出的时候需要注意的点。

有录友可能会想,ACM格式就是麻烦,有空格没有空格有什么影响,结果对了不就行了?

ACM模式相对于核心代码模式(力扣) 更考验大家对代码的掌控能力。 例如工程代码里,输出输出都是要自己控制的。这也是为什么大公司笔试,都是ACM模式。

以上代码中,结果都存在了 result数组里(二维数组,每一行是一个结果),最后将其打印出来。(重点看注释)

java 复制代码
// 输出结果
if (result.isEmpty()) System.out.println(-1);
for (List<Integer> pa : result) {
    for (int i = 0; i < pa.size() - 1; i++) {
        System.out.print(pa.get(i) + " ");
    }
    System.out.println(pa.get(pa.size() - 1));
}

本题代码

邻接矩阵写法:

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main {
    static List<List<Integer>> result = new ArrayList<>(); // 收集符合条件的路径
    static List<Integer> path = new ArrayList<>(); // 1节点到终点的路径

    public static void dfs(int[][] graph, int x, int n) {
        // 当前遍历的节点x 到达节点n
        if (x == n) { // 找到符合条件的一条路径
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点
            if (graph[x][i] == 1) { // 找到 x链接的节点
                path.add(i); // 遍历到的节点加入到路径中来
                dfs(graph, i, n); // 进入下一层递归
                path.remove(path.size() - 1); // 回溯,撤销本节点
            }
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();

        // 节点编号从1到n,所以申请 n+1 这么大的数组
        int[][] graph = new int[n + 1][n + 1];

        for (int i = 0; i < m; i++) {
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            // 使用邻接矩阵表示无向图,1 表示 s 与 t 是相连的
            graph[s][t] = 1;
        }

        path.add(1); // 无论什么路径已经是从1节点出发
        dfs(graph, 1, n); // 开始遍历

        // 输出结果
        if (result.isEmpty()) System.out.println(-1);
        for (List<Integer> pa : result) {
            for (int i = 0; i < pa.size() - 1; i++) {
                System.out.print(pa.get(i) + " ");
            }
            System.out.println(pa.get(pa.size() - 1));
        }
    }
}

邻接表写法

java 复制代码
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;

public class Main {
    static List<List<Integer>> result = new ArrayList<>(); // 收集符合条件的路径
    static List<Integer> path = new ArrayList<>(); // 1节点到终点的路径

    public static void dfs(List<LinkedList<Integer>> graph, int x, int n) {
        if (x == n) { // 找到符合条件的一条路径
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i : graph.get(x)) { // 找到 x指向的节点
            path.add(i); // 遍历到的节点加入到路径中来
            dfs(graph, i, n); // 进入下一层递归
            path.remove(path.size() - 1); // 回溯,撤销本节点
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();

        // 节点编号从1到n,所以申请 n+1 这么大的数组
        List<LinkedList<Integer>> graph = new ArrayList<>(n + 1);
        for (int i = 0; i <= n; i++) {
            graph.add(new LinkedList<>());
        }

        while (m-- > 0) {
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            // 使用邻接表表示 s -> t 是相连的
            graph.get(s).add(t);
        }

        path.add(1); // 无论什么路径已经是从1节点出发
        dfs(graph, 1, n); // 开始遍历

        // 输出结果
        if (result.isEmpty()) System.out.println(-1);
        for (List<Integer> pa : result) {
            for (int i = 0; i < pa.size() - 1; i++) {
                System.out.print(pa.get(i) + " ");
            }
            System.out.println(pa.get(pa.size() - 1));
        }
    }
}
相关推荐
.生产的驴15 分钟前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
爱上语文17 分钟前
宠物管理系统:Dao层
java·开发语言·宠物
吕小明么39 分钟前
OpenAI o3 “震撼” 发布后回归技术本身的审视与进一步思考
人工智能·深度学习·算法·aigc·agi
王ASC1 小时前
SpringMVC的URL组成,以及URI中对/斜杠的处理,解决IllegalStateException: Ambiguous mapping
java·mvc·springboot·web
是小崔啊1 小时前
开源轮子 - Apache Common
java·开源·apache
因我你好久不见1 小时前
springboot java ffmpeg 视频压缩、提取视频帧图片、获取视频分辨率
java·spring boot·ffmpeg
1 9 J1 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
程序员shen1616111 小时前
抖音短视频saas矩阵源码系统开发所需掌握的技术
java·前端·数据库·python·算法
汝即来归1 小时前
选择排序和冒泡排序;MySQL架构
数据结构·算法·排序算法
Ling_suu1 小时前
SpringBoot3——Web开发
java·服务器·前端