一题学习BFS和DFS算法
洛谷题目解析:【深基18.例3】查找文献
题目背景
小K热衷于在洛谷博客上阅读文章并探索其中的知识。每篇文章都可能包含指向其他博客文章的参考文献链接。小K的求知欲非常强,如果他阅读了某篇文章,他一定会去查看这篇文章的所有参考文献(但前提是这些参考文献他之前还没看过)。现在,我们帮助小K设计一种方法,确保他能不重复、不遗漏地阅读完所有他能看到的文章。
题目描述
洛谷博客共有n(n≤105)篇文章(编号为1到n)和m(m≤106)条参考文献引用关系。小K已经打开了编号为1的文章。我们需要通过深度优先搜索(DFS)和广度优先搜索(BFS)两种方式,来模拟小K阅读文章的过程,并输出遍历的结果。
输入输出格式
输入格式:
- 第一行包含两个整数n和m,分别表示文章总数和参考文献关系数。
- 接下来m行,每行包含两个整数X,Y,表示文章X有参考文献Y。
输出格式:
- 第一行输出DFS遍历的结果。
- 第二行输出BFS遍历的结果。
样例
样例输入:
8 9
1 2
1 3
1 4
2 5
2 6
3 7
4 7
4 8
7 8
样例输出:
1 2 5 6 3 7 8 4
1 2 3 4 5 6 7 8
解题思路
深度优先搜索(DFS)
DFS是一种用于遍历或搜索树或图的算法。它从根节点开始,尽可能深地搜索树的分支,直到达到叶子节点,然后回溯到前一个节点,继续探索尚未探索的分支。在DFS中,我们使用递归来实现这一过程。
广度优先搜索(BFS)
BFS也是一种遍历或搜索图或树的算法。它从根节点开始,首先访问根节点的所有相邻节点,然后对每个相邻节点,再访问它们的所有未被访问的相邻节点,以此类推,直到所有节点都被访问。在BFS中,我们通常使用队列来实现这一过程。
代码实现
以下是C++的实现代码,包含了DFS和BFS的具体实现:
#include<iostream>
#include<vector>
#include<queue>
#include<algorithm>
using namespace std;
const int MAXN = 100010;
vector<int> graph[MAXN];
bool visitedDFS[MAXN], visitedBFS[MAXN];
// DFS实现
void dfs(int node) {
if (visitedDFS[node]) return;
visitedDFS[node] = true;
cout << node << " ";
for (int neighbor : graph[node]) {
dfs(neighbor);
}
}
// BFS实现
void bfs(int start) {
queue<int> q;
q.push(start);
visitedBFS[start] = true;
while (!q.empty()) {
int current = q.front();
q.pop();
cout << current << " ";
for (int neighbor : graph[current]) {
if (!visitedBFS[neighbor]) {
q.push(neighbor);
visitedBFS[neighbor] = true;
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int from, to;
cin >> from >> to;
graph[from].push_back(to);
}
// 由于题目要求先访问编号较小的文章,我们不需要对邻接表排序
dfs(1);
cout << endl;
fill(visitedBFS, visitedBFS + MAXN, false); // 清除BFS的访问标记
bfs(1);
return 0;
}
当然,以下是这段代码中算法部分的详细讲解:
1. 图的表示
代码使用了一个邻接表(vector<int> g[MAX];
)来表示图。g[i]
是一个存储了所有与节点 i
相邻的节点编号的 vector
。这种表示法适用于稀疏图,因为它只存储实际存在的边,而不是为每对可能的节点都保留一个空间(如邻接矩阵)。
2. 深度优先搜索(DFS)
dfs
函数实现了深度优先搜索算法。它从给定的起始节点 u
开始,递归地探索图:
- 首先检查节点
u
是否已经被访问过(通过used[u]
数组)。如果是,则直接返回,避免重复访问。 - 将节点
u
标记为已访问(used[u] = 1;
)。 - 输出节点
u
的编号。 - 然后,对于节点
u
的每一个邻接节点e
,递归调用dfs(e)
。
3. 广度优先搜索(BFS)
bfs
函数实现了广度优先搜索算法。它从给定的起始节点 u
开始,逐层遍历图:
- 使用一个队列
q
来存储待访问的节点。首先将起始节点u
加入队列。 - 当队列不为空时,循环执行以下操作:
- 从队列中取出一个节点
u
(q.front()
),然后将其从队列中移除(q.pop()
)。 - 检查节点
u
是否已经被访问过(通过used2[u]
数组)。如果是,则跳过当前循环的剩余部分,继续处理下一个节点。 - 输出节点
u
的编号。 - 将节点
u
标记为已访问(used2[u] = 1;
)。 - 对于节点
u
的每一个邻接节点e
,将其加入队列q
中以便后续访问。
- 从队列中取出一个节点
4. 输入与输出
- 输入部分首先读取两个整数
a
和b
,其中a
表示图中节点的数量(尽管g
数组的大小已经通过MAX
定义为100010
,但a
可能用于后续验证或处理),b
表示边的数量。 - 然后,读入
b
条边,每条边由两个整数c
和d
表示,意味着存在一条从节点c
到节点d
的边。这些边被添加到邻接表中。 - 在遍历所有边之后,对每个节点的邻接节点进行排序(这可能是为了某种特定的输出顺序或后续处理的需要)。
- 最后,从节点
1
开始分别执行 DFS 和 BFS,并输出访问节点的顺序。DFS 的输出将展示从节点1
开始的一条深度优先遍历路径,而 BFS 的输出将展示从节点1
开始逐层向外扩展的广度优先遍历路径。
5. 注意事项
-
used
和used2
数组分别用于 DFS 和 BFS 的访问标记,以避免重复访问节点。 -
在实际应用中,如果图是有向的,则这种表示和遍历方法是有效的。如果图是无向的,那么可能需要在添加边时同时添加两个方向的边(即
g[c].push_back(d);
和g[d].push_back(c);
)。 -
排序邻接节点可能是为了特定的输出要求或算法优化,但在某些情况下可能不是必需的。
-
题目中提到“不保证编号为1的文章没有被其他文章引用”,但在这个问题中,我们是从编号为1的文章开始遍历,所以这一点不影响我们的遍历过程。
-
在DFS中,我们使用了递归函数来深入探索每一个分支。当遇到已经访问过的节点时,我们会直接返回,避免重复遍历。
-
在BFS中,我们使用了队列来存储待访问的节点。每次从队列中取出一个节点进行访问,并将其所有未访问的邻接节点加入队列中。这样可以保证我们总是先访问离起始节点最近的节点。
-
注意,在每次使用完DFS或BFS后,我们都需要重置访问标记数组,以便下一次遍历能够正确进行。但在本题中,由于我们只进行了一次DFS和一次BFS,所以只需在BFS前重置BFS的访问标记数组即可。
-
最后,由于题目要求输出编号较小的文章在前,而我们在读入边时,默认将每个节点的邻接节点按照输入顺序存储在了邻接表中。由于C++的
vector
在遍历时会按照元素被添加的顺序进行,所以自然满足了这个要求,我们无需对邻接表进行额外的排序操作。总结
通过这道题,我们复习了图的深度优先搜索(DFS)和广度优先搜索(BFS)两种遍历算法,并了解了它们在实际问题中的应用。同时,我们也学会了如何使用邻接表来表示图,并如何通过递归和队列来实现DFS和BFS的遍历过程。希望这次解析能够帮助大家更好地理解和掌握这两种重要的图遍历算法。