文章目录
- 图论理论基础(1)
-
- [1. 图的基本概念](#1. 图的基本概念)
-
- [1.1 基本术语](#1.1 基本术语)
- [2. 图的分类](#2. 图的分类)
-
- [2.1 有向图 vs 无向图](#2.1 有向图 vs 无向图)
- [2.2 加权图 vs 无权图](#2.2 加权图 vs 无权图)
- [2.3 度(Degree)](#2.3 度(Degree))
- [3. 图的连通性](#3. 图的连通性)
-
- [3.1 连通图 vs 非连通图](#3.1 连通图 vs 非连通图)
- [3.2 强连通性(有向图)](#3.2 强连通性(有向图))
- [3.3 连通分量(无向图)](#3.3 连通分量(无向图))
- [4. 图的存储方式](#4. 图的存储方式)
-
- [4.1 邻接矩阵](#4.1 邻接矩阵)
- [4.2 邻接表](#4.2 邻接表)
- [4.3 边列表](#4.3 边列表)
- [5. 图的遍历算法](#5. 图的遍历算法)
-
- [5.1 深度优先搜索(DFS)](#5.1 深度优先搜索(DFS))
- [5.2 广度优先搜索(BFS)](#5.2 广度优先搜索(BFS))
- [6. 图论算法分类](#6. 图论算法分类)
-
- [6.1 图的存储与遍历](#6.1 图的存储与遍历)
- [6.2 拓扑排序](#6.2 拓扑排序)
- [6.3 并查集](#6.3 并查集)
- [6.4 最短路径算法](#6.4 最短路径算法)
- [6.5 最小生成树](#6.5 最小生成树)
- [7. 时间复杂度总结](#7. 时间复杂度总结)
- [8. DFS与BFS对比](#8. DFS与BFS对比)
-
- [8.1 算法对比](#8.1 算法对比)
- [8.2 DFS搜索过程](#8.2 DFS搜索过程)
- [8.3 DFS代码框架](#8.3 DFS代码框架)
- [9. BFS详解](#9. BFS详解)
-
- [9.1 BFS使用场景](#9.1 BFS使用场景)
- [9.2 BFS搜索过程](#9.2 BFS搜索过程)
- [9.3 BFS代码框架](#9.3 BFS代码框架)
图论理论基础(1)
1. 图的基本概念
**图(Graph)**是由顶点(Vertex/Node)和边(Edge)组成的数据结构,用来表示事物之间的关系。
1.1 基本术语
- 顶点(Vertex):图中的节点,表示事物
- 边(Edge):连接两个顶点的线,表示关系
- 路径(Path):从一个顶点到另一个顶点经过的边的序列
- 环(Cycle):起点和终点相同的路径
2. 图的分类
2.1 有向图 vs 无向图
- 无向图:边没有方向,A-B 和 B-A 是同一条边
- 有向图:边有方向,A→B 和 B→A 是不同的边
2.2 加权图 vs 无权图
- 无权图:边没有权重,只表示连接关系
- 加权图:边有权重,表示距离、成本等
2.3 度(Degree)
- 度:与顶点相连的边的数量
- 出度:有向图中从顶点出发的边的数量
- 入度:有向图中指向顶点的边的数量
3. 图的连通性
3.1 连通图 vs 非连通图
- 连通图:任意两个顶点之间都存在路径
- 非连通图:存在两个顶点之间没有路径
3.2 强连通性(有向图)
- 强连通图:任意两个顶点之间都存在双向路径
- 强连通分量:极大强连通子图
示例:
有向图:A → B → C → A
↓
D
强连通分量1:{A, B, C} - 这三个节点互相可达
强连通分量2:{D} - 单独一个节点
图解:
A ──→ B ──→ C
│ │
│ │
└───────────┘
│
↓
D
3.3 连通分量(无向图)
- 连通分量:无向图中的极大连通子图
- 连通分量数量:图中连通分量的个数
示例:
无向图:A-B-C, D-E, F
连通分量1:{A, B, C} - 这三个节点互相连通
连通分量2:{D, E} - 这两个节点互相连通
连通分量3:{F} - 单独一个节点
连通分量数量:3
图解:
A ── B ── C D ── E F
4. 图的存储方式
4.1 邻接矩阵
cpp
// 适合稠密图,空间复杂度O(V²)
vector<vector<int>> graph(V, vector<int>(V, 0));
// graph[i][j] = 1 表示顶点i和j之间有边
4.2 邻接表
cpp
// 适合稀疏图,空间复杂度O(V+E)
vector<vector<int>> graph(V);
// graph[i] 存储与顶点i相邻的所有顶点
4.3 边列表
cpp
// 存储所有边的信息
vector<pair<int, int>> edges; // 无权边
vector<tuple<int, int, int>> edges; // 加权边 (u, v, weight)
5. 图的遍历算法
5.1 深度优先搜索(DFS)
算法特点:
- 深度优先:尽可能深地搜索图的分支
- 递归实现:使用递归栈,代码简洁
- 应用场景:路径查找、连通性判断、拓扑排序
- 时间复杂度:O(V+E),每个顶点和边访问一次
- 空间复杂度:O(V),递归栈的深度
执行过程示例:
图:A-B-C
| |
D E
DFS访问顺序:A → B → C → E → D
代码实现:
cpp
void dfs(int node, vector<bool>& visited, vector<vector<int>>& graph) {
visited[node] = true; // 标记当前节点已访问
// 遍历当前节点的所有邻居
for (int neighbor : graph[node]) {
if (!visited[neighbor]) { // 如果邻居未被访问
dfs(neighbor, visited, graph); // 递归访问邻居
}
}
}
5.2 广度优先搜索(BFS)
算法特点:
- 广度优先:逐层访问,先访问距离近的节点
- 队列实现:使用队列维护访问顺序
- 应用场景:最短路径、层次遍历、连通性判断
- 时间复杂度:O(V+E),每个顶点和边访问一次
- 空间复杂度:O(V),队列的最大长度
执行过程示例:
图:A-B-C
| |
D E
BFS访问顺序:A → B → D → C → E
层次:第0层:A
第1层:B, D
第2层:C, E
代码实现:
cpp
void bfs(int start, vector<vector<int>>& graph) {
queue<int> q; // 队列存储待访问的节点
vector<bool> visited(graph.size(), false); // 标记数组
q.push(start); // 将起始节点加入队列
visited[start] = true; // 标记起始节点已访问
while (!q.empty()) { // 队列不为空时继续
int node = q.front(); // 取出队首节点
q.pop(); // 从队列中移除
// 遍历当前节点的所有邻居
for (int neighbor : graph[node]) {
if (!visited[neighbor]) { // 如果邻居未被访问
visited[neighbor] = true; // 标记邻居已访问
q.push(neighbor); // 将邻居加入队列
}
}
}
}
6. 图论算法分类
6.1 图的存储与遍历
- DFS/BFS:基础遍历算法
- 应用:岛屿数量、被围绕的区域
6.2 拓扑排序
- Kahn算法:基于入度的拓扑排序
- DFS法:基于深度优先搜索的拓扑排序
- 应用:课程表、任务调度
6.3 并查集
- Union-Find:集合合并与查询
- 应用:省份数量、朋友圈
6.4 最短路径算法
- Dijkstra:单源最短路径(非负权)
- Bellman-Ford:单源最短路径(可处理负权)
- Floyd-Warshall:多源最短路径
6.5 最小生成树
- Kruskal:基于边的贪心算法
- Prim:基于顶点的贪心算法
7. 时间复杂度总结
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| DFS/BFS | O(V+E) | O(V) | 图遍历 |
| 拓扑排序 | O(V+E) | O(V) | DAG排序 |
| 并查集 | O(α(n)) | O(V) | 连通性判断 |
| Dijkstra | O((V+E)logV) | O(V) | 单源最短路 |
| Floyd | O(V³) | O(V²) | 多源最短路 |
| Kruskal | O(ElogE) | O(V) | 最小生成树 |
8. DFS与BFS对比
8.1 算法对比
| 比较项 | DFS(深度优先搜索) | BFS(广度优先搜索) |
|---|---|---|
| 搜索策略 | 一条路走到黑(递归/栈) | 一层一层扩展(队列) |
| 实现方式 | 通常用 递归 或 显式栈 | 通常用 队列(queue) |
| 适用场景 | 适合 全路径搜索、组合问题、回溯问题(如图遍历、排列组合) | 适合 最短路径问题、层次遍历(如最短步数、层数搜索) |
| 空间复杂度 | 相对较小(与递归深度有关) | 相对较大(需存整层节点) |
| 典型应用 | 迷宫求解、图连通分量、全排列 | 最短路径、最少步数、层序遍历 |
8.2 DFS搜索过程
假设你在一个迷宫中:
- 看到一个路口 → 选一条路走;
- 走到底发现死路 → 返回上一个岔口;
- 选另一条没走过的路 → 再次深入;
- 最终找到所有可能的出口。
这整个过程就叫 深度优先搜索(DFS)。
8.3 DFS代码框架
cpp
vector<vector<int>> result; // 保存符合条件的所有路径
vector<int> path; // 起点到终点的路径
void dfs (参数){
if (终止条件) {
存放结果;
return;
}
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}
9. BFS详解
9.1 BFS使用场景
广度优先搜索(Breadth First Search,简称 BFS)是一种逐层搜索的算法,常用于在图或树结构中寻找最短路径或最小步数的问题。
常见使用场景:
- 最短路径问题 :
例如在无权图中,寻找从起点到终点的最短路径。- 典型例题:二叉树的最短深度、迷宫最短路径、网络中最短连接。
- 层序遍历 :
BFS 天然按层扩展节点,因此在树结构中可用于层序遍历。 - 状态搜索类问题 :
如八数码问题、单词接龙、棋盘问题等,BFS 可用于寻找最少操作次数。 - 网络流与最短增广路 :
在最大流算法(如 Edmonds-Karp)中,BFS 用于寻找最短增广路径。 - 多源最短路问题 :
同时从多个起点出发,计算所有点到最近起点的最短距离。
9.2 BFS搜索过程
BFS 采用队列(queue)实现"先进先出"的搜索逻辑,从起点开始,依次访问相邻节点,逐层向外扩展。
搜索流程:
- 将起点加入队列,并标记为已访问;
- 当队列非空时:
- 取出队首节点;
- 遍历该节点的所有邻接节点;
- 若邻接节点未访问,则加入队列并标记;
- 直到队列为空或找到目标。
9.3 BFS代码框架
cpp
int dir[4][2] = {
{0, 1}, {1, 0}, {0, -1}, {-1, 0}
};// 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
queue<pair<int, int>> que; // 定义队列
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; // 只要加入队列立刻标记,避免重复访问
}
}
}
}