图论理论基础(1)
文章目录
- 图论理论基础(1)
- 1. 图的基本概念
- 1.1 基本术语
- 2. 图的分类
- 2.1 有向图 vs 无向图
- 2.2 加权图 vs 无权图
- 2.3 度(Degree)
- 3. 图的连通性
- 3.1 连通图 vs 非连通图
- 3.2 强连通性(有向图)
- 3.3 连通分量(无向图)
- 4. 图的存储方式
- 4.1 邻接矩阵
- 4.2 邻接表
- 4.3 边列表
- 5. 图的遍历算法
- 5.1 深度优先搜索(DFS)
- 5.2 广度优先搜索(BFS)
- 6. 图论算法分类
- 6.1 图的存储与遍历
- 6.2 拓扑排序
- 6.3 并查集
- 6.4 最短路径算法
- 6.5 最小生成树
- 7. 时间复杂度总结
- 8. DFS与BFS对比
- 8.1 算法对比
- 8.2 DFS搜索过程
- 8.3 DFS代码框架
- 9. BFS详解
- 9.1 BFS使用场景
- 9.2 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 邻接矩阵
// 适合稠密图,空间复杂度O(V²)
vector<vector<int>> graph(V, vector<int>(V, 0));
// graph[i][j] = 1 表示顶点i和j之间有边
4.2 邻接表
// 适合稀疏图,空间复杂度O(V+E)
vector<vector<int>> graph(V);
// graph[i] 存储与顶点i相邻的所有顶点
4.3 边列表
// 存储所有边的信息
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 EDFS访问顺序:A → B → C → E → D
代码实现:
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 EBFS访问顺序:A → B → D → C → E
层次:第0层:A第1层:B, D第2层:C, E
代码实现:
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代码框架
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代码框架
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; // 只要加入队列立刻标记,避免重复访问}}}}