一、图论基础概念
-
图的定义
一个图 GGG 由两个集合组成:
- 顶点集合 VVV (Vertices):代表图中的节点或实体。
- 边集合 EEE (Edges):代表顶点之间的连接或关系。每条边连接两个顶点(或自身,形成自环)。
-
图的分类
- 无向图 (Undirected Graph) :边没有方向。如果顶点 uuu 和 vvv 之间有边,则 uuu 和 vvv 是相邻的,边表示为 (u,v)(u, v)(u,v) 或 (v,u)(v, u)(v,u)。
- 有向图 (Directed Graph / Digraph) :边有方向。从顶点 uuu 指向顶点 vvv 的边表示为 <u,v><u, v><u,v> 或 u→vu \to vu→v。uuu 是尾 (tail),vvv 是头 (head)。
- 加权图 (Weighted Graph):边(有时也包括顶点)被赋予一个数值(权重),表示距离、成本、容量等属性。
- 无权图 (Unweighted Graph):边没有权重,通常认为权重为 1。
-
基本术语
- 邻接 (Adjacency):如果两个顶点之间有边直接相连,则它们是邻接的。
- 度 (Degree) :
- 无向图中,一个顶点的度是其邻接边的数量。
- 有向图中:
- 出度 (Out-degree):从该顶点出发的边的数量。
- 入度 (In-degree):指向该顶点的边的数量。
- 路径 (Path) :顶点序列 v0,v1,v2,...,vkv_0, v_1, v_2, \dots, v_kv0,v1,v2,...,vk,其中每条边 (vi,vi+1)(v_i, v_{i+1})(vi,vi+1)(无向图)或 <vi,vi+1><v_i, v_{i+1}><vi,vi+1>(有向图)都存在。
- 环/圈 (Cycle):起点和终点相同的路径(且路径长度至少为 1)。
- 连通性 (Connectivity) :
- 无向图:如果任意两个顶点之间都存在路径,则该图是连通的 (Connected)。一个图可能有多个连通分量 (Connected Components)。
- 有向图:强连通 (Strongly Connected) 指任意两个顶点 uuu 和 vvv 之间都存在从 uuu 到 vvv 和从 vvv 到 uuu 的路径。弱连通 (Weakly Connected) 指忽略边方向后对应的无向图是连通的。
二、图的表示方法 (C++ 实现)
在 C++ 中,常用的图表示方法有:
-
邻接矩阵 (Adjacency Matrix)
-
原理 :使用一个二维数组
matrix[V][V]表示图。对于无权图:matrix[u][v] = 1表示存在边 (u,v)(u, v)(u,v) (无向图) 或 <u,v><u, v><u,v> (有向图)。matrix[u][v] = 0表示无边。
-
加权图 :
matrix[u][v]存储边的权重。若无边,可用特定值(如INT_MAX)或0(需注意区分)表示。 -
C++ 代码示例:
cpp#include <iostream> #include <vector> using namespace std; class Graph { private: int V; // 顶点数 vector<vector<int>> adjMatrix; // 邻接矩阵 public: Graph(int vertices) : V(vertices) { adjMatrix.resize(V, vector<int>(V, 0)); // 初始化全0 } // 添加无向图边 (无权) void addEdgeUndirected(int u, int v) { adjMatrix[u][v] = 1; adjMatrix[v][u] = 1; // 无向图是对称的 } // 添加有向图边 (无权) void addEdgeDirected(int u, int v) { adjMatrix[u][v] = 1; } // 添加带权有向边 void addWeightedEdge(int u, int v, int weight) { adjMatrix[u][v] = weight; } // 打印邻接矩阵 void print() { for (int i = 0; i < V; ++i) { for (int j = 0; j < V; ++j) { cout << adjMatrix[i][j] << " "; } cout << endl; } } }; -
优缺点:
- 优点 :查询边是否存在很快 O(1)O(1)O(1)。
- 缺点 :空间复杂度高 O(V2)O(V^2)O(V2),不适用于稀疏图。添加/删除顶点开销大。
-
-
邻接表 (Adjacency List)
-
原理:为每个顶点维护一个列表(链表、动态数组等),存储与该顶点直接相邻的所有顶点(对于加权图,还需存储权重)。
-
C++ 代码示例 (使用
vector存储列表):cpp#include <iostream> #include <vector> using namespace std; // 加权图的边结构体 struct Edge { int dest; // 目标顶点 int weight; // 权重 Edge(int d, int w) : dest(d), weight(w) {} }; class Graph { private: int V; // 顶点数 vector<vector<Edge>> adjList; // 邻接表 (存储边) public: Graph(int vertices) : V(vertices) { adjList.resize(V); } // 添加无向图边 (无权) void addEdgeUndirected(int u, int v) { adjList[u].push_back(Edge(v, 1)); // 权重设为1 adjList[v].push_back(Edge(u, 1)); } // 添加有向图边 (无权) void addEdgeDirected(int u, int v) { adjList[u].push_back(Edge(v, 1)); } // 添加带权有向边 void addWeightedEdge(int u, int v, int weight) { adjList[u].push_back(Edge(v, weight)); } // 打印邻接表 void print() { for (int i = 0; i < V; ++i) { cout << "Vertex " << i << ": "; for (const Edge& edge : adjList[i]) { cout << "-> (" << edge.dest << ", " << edge.weight << ") "; } cout << endl; } } }; -
优缺点:
- 优点 :空间复杂度低 O(V+E)O(V + E)O(V+E),适用于稀疏图。易于遍历一个顶点的所有邻居。
- 缺点 :查询任意两点间是否有边稍慢 O(deg(u))O(\text{deg}(u))O(deg(u))(平均)。
-
选择哪种表示方法? 取决于图的稀疏程度和需要频繁执行的操作。稀疏图优先考虑邻接表。
三、图遍历算法
遍历是图算法的基础,用于访问图中所有顶点(或连通分量)。
-
广度优先搜索 (BFS - Breadth-First Search)
-
原理:从源点开始,逐层向外探索,先访问所有距离为 1 的邻居,再访问距离为 2 的邻居,以此类推。使用队列 (Queue) 管理待访问顶点。
-
应用:无权图的最短路径(最少边数)、连通分量、社交网络中的好友层级。
-
C++ 实现 (邻接表,无权图) :
cpp#include <iostream> #include <vector> #include <queue> using namespace std; void BFS(const vector<vector<int>>& graph, int start) { int V = graph.size(); vector<bool> visited(V, false); // 标记是否访问过 queue<int> q; visited[start] = true; q.push(start); while (!q.empty()) { int u = q.front(); q.pop(); cout << u << " "; // 访问当前顶点 // 遍历 u 的所有邻居 for (int v : graph[u]) { if (!visited[v]) { visited[v] = true; q.push(v); } } } }
-
-
深度优先搜索 (DFS - Depth-First Search)
-
原理:沿着一条路径尽可能深入地探索,直到尽头,然后回溯。使用递归或栈 (Stack) 实现。
-
应用:查找连通分量、拓扑排序、查找环、路径探索(迷宫求解)。
-
C++ 递归实现 (邻接表) :
cpp#include <iostream> #include <vector> using namespace std; void DFSRecursive(const vector<vector<int>>& graph, int u, vector<bool>& visited) { visited[u] = true; cout << u << " "; // 访问当前顶点 for (int v : graph[u]) { if (!visited[v]) { DFSRecursive(graph, v, visited); } } } void DFS(const vector<vector<int>>& graph, int start) { int V = graph.size(); vector<bool> visited(V, false); DFSRecursive(graph, start, visited); }
-
四、实际应用与算法
-
最短路径问题
- 问题描述:找到图中两个顶点之间权重总和最小的路径。
- 算法 :
- 无权图:BFS 即可。
- 带权图 (无负权边) :Dijkstra 算法。使用优先队列(最小堆)选择当前距离最小的未访问顶点。
- 带权图 (允许负权边) :Bellman-Ford 算法 。进行 V−1V-1V−1 轮松弛操作。
- 所有点对间最短路径 :Floyd-Warshall 算法。动态规划。
-
最小生成树 (MST - Minimum Spanning Tree)
- 问题描述:在连通的无向加权图中,找到一棵树,连接所有顶点,且边的权重总和最小。
- 算法 :
- Prim 算法:从任意顶点开始,每次选择连接当前树与树外顶点且权重最小的边。使用优先队列。
- Kruskal 算法:将所有边按权重排序,从小到大依次选择,若加入该边不会形成环(使用并查集判断)则加入 MST。
-
拓扑排序 (Topological Sorting)
- 问题描述 :对有向无环图 (DAG) 的所有顶点进行线性排序,使得对于每条有向边 <u,v><u, v><u,v>,uuu 在排序中都出现在 vvv 之前。
- 应用:任务调度、课程安排、编译依赖。
- 算法:基于 DFS 或 BFS (Kahn's 算法 - 基于入度)。
-
网络流问题
- 问题描述:建模网络中的流量传输(如水管、交通、数据传输),计算最大流量或最小割。
- 核心算法 :Ford-Fulkerson 方法 ,其核心是 Edmonds-Karp 算法(使用 BFS 寻找增广路径)。
五、总结
图论提供了强大的工具来建模复杂关系和解决优化问题。在 C++ 中,邻接矩阵和邻接表是两种核心的存储结构。BFS 和 DFS 是遍历的基础。Dijkstra、Bellman-Ford、Prim、Kruskal、拓扑排序和网络流算法等则解决了图论中的经典问题。理解这些基础理论和掌握它们的 C++ 实现,对于解决涉及网络、关系、路径和优化的实际问题至关重要。