1. 题目背景与核心考察点解析
洛谷P5318【深基18.例3】查找文献是一道经典的图论搜索基础练习题,主要考察DFS(深度优先搜索)和BFS(广度优先搜索)两种基础算法的实现能力。题目要求对给定的文献引用关系图进行遍历,并分别输出DFS和BFS的访问顺序。
这道题在算法竞赛入门阶段具有典型意义:
- 它来自《深入浅出程序设计竞赛(基础篇)》的配套练习
- 考察点纯粹聚焦于搜索算法的标准实现
- 输入输出格式规范,适合作为算法模板题
- 数据规模适中(n≤10^5),需要考虑算法效率
提示:虽然题目名为"查找文献",但实际考察的是标准图的遍历算法,不要被表面描述迷惑而想复杂了。
2. 数据结构设计与输入处理
2.1 图的存储方式选择
对于n≤100,000的规模,我们需要选择高效的邻接表存储方式。常见的实现方案有:
cpp复制vector<vector<int>> adj(n+1); // C++版本
python复制adj = [[] for _ in range(n+1)] # Python版本
选择理由:
- 空间复杂度O(V+E),适合稀疏图
- 可以方便地进行升序排序(题目要求)
- 访问相邻节点的时间复杂度为O(1)
2.2 输入处理细节
输入格式为:
code复制n m
u1 v1
u2 v2
...
um vm
关键处理步骤:
- 读取n和m时要注意可能的多个空格分隔
- 对于无向图需要双向建边(题目未明确时需要确认)
- 存储后需要对每个节点的邻接表进行排序(题目要求按编号升序访问)
python复制n, m = map(int, input().split())
adj = [[] for _ in range(n+1)]
for _ in range(m):
u, v = map(int, input().split())
adj[u].append(v)
adj[v].append(u) # 如果是无向图
for neighbors in adj:
neighbors.sort() # 满足升序访问要求
3. DFS算法实现与优化
3.1 标准DFS实现
深度优先搜索的递归实现模板:
python复制visited = [False] * (n+1)
def dfs(u):
print(u, end=' ')
visited[u] = True
for v in adj[u]:
if not visited[v]:
dfs(v)
3.2 栈实现的迭代版本
对于大规模数据,递归可能导致栈溢出,此时需要迭代实现:
cpp复制void dfs(int start) {
stack<int> s;
vector<bool> visited(n+1, false);
s.push(start);
visited[start] = true;
cout << start << " ";
while (!s.empty()) {
int u = s.top();
bool hasUnvisited = false;
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
cout << v << " ";
s.push(v);
hasUnvisited = true;
break;
}
}
if (!hasUnvisited) {
s.pop();
}
}
}
3.3 DFS的易错点分析
- 访问标记的时机:应该在入栈/递归时立即标记,而非出栈时
- 多连通分量处理:题目未说明图是否连通时,需要从每个未访问节点启动DFS
- 输出格式:行末空格问题,建议使用如下输出方式:
python复制print(' '.join(map(str, dfs_order)))
4. BFS算法实现与层序特性
4.1 标准BFS实现
广度优先搜索的队列实现模板:
python复制from collections import deque
def bfs(start):
visited = [False] * (n+1)
q = deque([start])
visited[start] = True
order = []
while q:
u = q.popleft()
order.append(u)
for v in adj[u]:
if not visited[v]:
visited[v] = True
q.append(v)
print(' '.join(map(str, order)))
4.2 BFS的层序遍历特性
BFS天然具有层序特性,可以方便地计算最短路径(无权图)。虽然本题不要求,但作为重要特性需要了解:
cpp复制void bfs(int start) {
queue<int> q;
vector<int> dist(n+1, -1);
q.push(start);
dist[start] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : adj[u]) {
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
q.push(v);
}
}
}
}
4.3 BFS实现中的常见问题
- 队列选择:避免使用list.pop(0)(O(n)时间复杂度)
- 重复入队:必须在入队时标记visited,而非出队时
- 内存分配:对于大规模图,预分配visited数组比动态扩容更高效
5. 完整AC代码与性能分析
5.1 Python完整实现
python复制import sys
from collections import deque
def main():
input = sys.stdin.read
data = input().split()
idx = 0
n, m = int(data[idx]), int(data[idx+1])
idx += 2
adj = [[] for _ in range(n+1)]
for _ in range(m):
u, v = int(data[idx]), int(data[idx+1])
adj[u].append(v)
adj[v].append(u)
idx += 2
for neighbors in adj:
neighbors.sort()
# DFS
visited = [False] * (n+1)
dfs_order = []
stack = [1]
visited[1] = True
while stack:
u = stack.pop()
dfs_order.append(u)
for v in reversed(adj[u]):
if not visited[v]:
visited[v] = True
stack.append(v)
print(' '.join(map(str, dfs_order)))
# BFS
visited = [False] * (n+1)
bfs_order = []
q = deque([1])
visited[1] = True
while q:
u = q.popleft()
bfs_order.append(u)
for v in adj[u]:
if not visited[v]:
visited[v] = True
q.append(v)
print(' '.join(map(str, bfs_order)))
if __name__ == "__main__":
main()
5.2 C++完整实现
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<vector<int>> adj(n+1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (auto& neighbors : adj) {
sort(neighbors.begin(), neighbors.end());
}
// DFS
vector<bool> visited(n+1, false);
stack<int> s;
s.push(1);
visited[1] = true;
vector<int> dfs_order;
while (!s.empty()) {
int u = s.top();
s.pop();
dfs_order.push_back(u);
for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
int v = *it;
if (!visited[v]) {
visited[v] = true;
s.push(v);
}
}
}
for (size_t i = 0; i < dfs_order.size(); ++i) {
if (i != 0) cout << " ";
cout << dfs_order[i];
}
cout << "\n";
// BFS
fill(visited.begin(), visited.end(), false);
queue<int> q;
q.push(1);
visited[1] = true;
vector<int> bfs_order;
while (!q.empty()) {
int u = q.front();
q.pop();
bfs_order.push_back(u);
for (int v : adj[u]) {
if (!visited[v]) {
visited[v] = true;
q.push(v);
}
}
}
for (size_t i = 0; i < bfs_order.size(); ++i) {
if (i != 0) cout << " ";
cout << bfs_order[i];
}
cout << "\n";
return 0;
}
5.3 复杂度分析与优化空间
时间复杂度:
- 建图:O(m)
- 排序:O(n log d),d为平均度数
- DFS/BFS:O(n + m)
- 总体:O(n log d + m)
空间复杂度:
- 邻接表:O(n + m)
- 辅助数组:O(n)
优化方向:
- 对于固定格式输入,使用更快的IO方式(如C++的ios::sync_with_stdio)
- 预先分配足够内存避免动态扩容
- 在BFS中可以使用循环队列进一步优化
6. 搜索算法的扩展应用
6.1 拓扑排序问题
虽然本题是无向图,但DFS和BFS在有向无环图(DAG)的拓扑排序中也有重要应用:
python复制def topological_sort(adj):
in_degree = [0] * (n+1)
for u in range(1, n+1):
for v in adj[u]:
in_degree[v] += 1
q = deque()
for u in range(1, n+1):
if in_degree[u] == 0:
q.append(u)
topo_order = []
while q:
u = q.popleft()
topo_order.append(u)
for v in adj[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
q.append(v)
return topo_order
6.2 连通分量检测
通过多次调用DFS/BFS可以找出图中的所有连通分量:
cpp复制int count_components() {
vector<bool> visited(n+1, false);
int components = 0;
for (int u = 1; u <= n; ++u) {
if (!visited[u]) {
components++;
queue<int> q;
q.push(u);
visited[u] = true;
while (!q.empty()) {
int v = q.front();
q.pop();
for (int w : adj[v]) {
if (!visited[w]) {
visited[w] = true;
q.push(w);
}
}
}
}
}
return components;
}
6.3 二分图检测
通过BFS/DFS可以检测图是否为二分图:
python复制def is_bipartite(adj):
color = [0] * (n+1)
q = deque([1])
color[1] = 1
while q:
u = q.popleft()
for v in adj[u]:
if color[v] == 0:
color[v] = 3 - color[u]
q.append(v)
elif color[v] == color[u]:
return False
return True
7. 刷题进阶建议与学习路径
7.1 搜索算法进阶路线
-
基础变形:
- 洛谷P1219 八皇后问题
- 洛谷P2392 考前临时抱佛脚
- 洛谷P1433 吃奶酪
-
记忆化搜索:
- 洛谷P1434 滑雪
- 洛谷P1464 Function
-
双向BFS:
- 洛谷P1379 八数码难题
-
启发式搜索:
- 洛谷P2324 骑士精神
- 洛谷P1378 油滴扩展
7.2 调试技巧与测试用例设计
针对搜索类题目的常见调试方法:
- 小规模测试:手动构造n=3,4的测试用例
- 边界测试:空图、单节点、链状图、完全图
- 可视化调试:打印搜索过程中的状态变化
- 对拍验证:用DFS和BFS结果交叉验证
示例测试用例:
code复制# 样例1:简单链状图
3 2
1 2
2 3
# 样例2:完全图
4 6
1 2
1 3
1 4
2 3
2 4
3 4
# 样例3:非连通图
5 3
1 2
3 4
4 5
7.3 竞赛中的搜索优化策略
- 状态压缩:使用位运算表示状态
- 剪枝技巧:
- 可行性剪枝
- 最优性剪枝
- 对称性剪枝
- 迭代加深(IDDFS)
- 双向搜索
- A*算法与估价函数设计
经验分享:在实际比赛中,当遇到看似复杂的题目时,先考虑是否可以通过搜索暴力解决。很多题目正解确实是搜索,或者搜索可以作为验证其他算法正确性的工具。
