1. 题目背景与需求解析
"查找文献"这道题目出现在洛谷的"深基18.例3",属于算法竞赛中图论基础知识的经典练习题。题目要求实现一个简单的文献引用关系管理系统,核心是处理有向图的基本遍历操作。
在实际科研场景中,文献之间的引用关系构成了复杂的网络。比如论文A引用了论文B,就可以表示为一条从A指向B的有向边。我们需要对这样的关系网络进行系统化遍历,这正是拓扑排序和图的遍历算法的典型应用场景。
2. 输入输出格式分析
题目通常会给出这样的输入格式:
- 第一行两个整数n和m,表示文献数量(顶点数)和引用关系数量(边数)
- 接下来m行,每行两个整数u和v,表示文献u引用了文献v
输出要求:
- 先输出DFS遍历结果
- 再输出BFS遍历结果
- 对于同一层级的文献,按照编号从小到大访问
例如输入:
code复制5 5
1 2
1 3
2 4
3 4
4 5
对应图结构为:
code复制1 → 2 → 4 → 5
1 → 3 → 4 → 5
3. 图的存储结构选择
3.1 邻接表实现
对于稀疏图(边数远小于n²的情况),邻接表是最佳选择。我们可以用vector数组来表示:
cpp复制vector<int> graph[MAXN]; // MAXN根据题目数据范围设定
建图过程:
cpp复制int n, m;
cin >> n >> m;
while(m--) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
}
3.2 邻接矩阵的适用性
虽然邻接矩阵(二维数组)实现简单:
cpp复制bool graph[MAXN][MAXN];
但对于n较大的情况(如n=1e5),空间复杂度O(n²)将无法承受。在算法竞赛中,除非题目明确给出小数据范围,否则不建议使用。
4. DFS遍历实现细节
4.1 递归实现
cpp复制bool visited[MAXN];
void dfs(int u) {
if(visited[u]) return;
visited[u] = true;
cout << u << " "; // 访问节点
for(int v : graph[u]) {
dfs(v);
}
}
4.2 非递归实现(栈)
cpp复制void dfs_stack(int start) {
stack<int> s;
bool visited[MAXN] = {false};
s.push(start);
visited[start] = true;
while(!s.empty()) {
int u = s.top();
s.pop();
cout << u << " ";
// 注意邻接点要逆序入栈
for(auto it = graph[u].rbegin(); it != graph[u].rend(); ++it) {
int v = *it;
if(!visited[v]) {
visited[v] = true;
s.push(v);
}
}
}
}
4.3 处理节点顺序
题目要求"对于同一层的节点按编号从小到大访问",因此建图后需要对邻接表排序:
cpp复制for(int i = 1; i <= n; ++i) {
sort(graph[i].begin(), graph[i].end());
}
5. BFS遍历实现要点
5.1 标准队列实现
cpp复制void bfs(int start) {
queue<int> q;
bool visited[MAXN] = {false};
q.push(start);
visited[start] = true;
while(!q.empty()) {
int u = q.front();
q.pop();
cout << u << " ";
for(int v : graph[u]) {
if(!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
5.2 层级遍历的变种
如果需要记录每个节点的层级(如求最短引用链长度),可以改进为:
cpp复制int level[MAXN] = {0};
void bfs_level(int start) {
queue<int> q;
q.push(start);
level[start] = 1;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int v : graph[u]) {
if(level[v] == 0) {
level[v] = level[u] + 1;
q.push(v);
}
}
}
}
6. 完整代码实现
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
const int MAXN = 1e5 + 5;
vector<int> graph[MAXN];
bool visited[MAXN];
void dfs(int u) {
if(visited[u]) return;
visited[u] = true;
cout << u << " ";
for(int v : graph[u]) {
dfs(v);
}
}
void bfs(int start) {
queue<int> q;
bool visited[MAXN] = {false};
q.push(start);
visited[start] = true;
while(!q.empty()) {
int u = q.front();
q.pop();
cout << u << " ";
for(int v : graph[u]) {
if(!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
for(int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
}
// 排序保证访问顺序
for(int i = 1; i <= n; ++i) {
sort(graph[i].begin(), graph[i].end());
}
// DFS
fill(visited, visited + MAXN, false);
dfs(1);
cout << endl;
// BFS
bfs(1);
cout << endl;
return 0;
}
7. 常见问题与调试技巧
7.1 栈溢出问题
当图的深度很大时(如链式结构),递归DFS可能导致栈溢出。解决方法:
- 改用非递归实现
- 在编译时增加栈大小(如G++使用-Wl,--stack=16777216)
7.2 重复访问判断
在BFS中,节点可能在入队前被多次发现,但只需入队一次。常见错误是:
cpp复制// 错误写法:可能导致重复入队
if(!visited[v]) {
q.push(v);
visited[v] = true; // 应该在入队时立即标记
}
7.3 孤立节点的处理
题目没有明确说明是否所有节点都连通。稳妥的做法是:
cpp复制for(int i = 1; i <= n; ++i) {
if(!visited[i]) {
dfs(i); // 或 bfs(i)
}
}
8. 算法复杂度分析
设n为顶点数,m为边数:
- 时间复杂度:
- 排序:O(m log d),d为平均出度
- DFS/BFS:O(n + m)
- 空间复杂度:
- 邻接表存储:O(n + m)
- 访问数组:O(n)
9. 实际应用扩展
这个基础算法可以扩展解决许多实际问题:
- 文献引用网络分析(找出核心文献)
- 网页爬虫的URL抓取策略
- 社交网络的好友推荐
- 编译器中的依赖关系解析
例如在科研中,可以用类似算法找出一个领域的奠基性文献(入度为0的节点)或关键桥梁文献(在多个路径中出现的节点)。
10. 竞赛中的优化技巧
- 使用更快的IO方式:
cpp复制ios::sync_with_stdio(false);
cin.tie(0);
- 用数组替代vector(在固定规模问题时)
- 使用位运算压缩访问标记(当n很大时)
- 预处理阶段就完成排序,避免多次排序
对于超大规模图(如n>1e6),需要考虑:
- 内存映射文件处理
- 外部排序算法
- 并行化遍历
