图论是计算机科学中的一个重要领域,研究图(Graph)的结构、性质以及相关算法。图由**顶点(Vertex)和边(Edge)**组成,广泛应用于社交网络、路径规划、网络流等问题。
1. 图的基本概念
图的定义
-
图(Graph):由一组顶点 VV 和一组边 EE 组成,记作 G=(V,E)G=(V,E)。
-
有向图(Directed Graph):边有方向。
-
无向图(Undirected Graph):边无方向。
-
权重图(Weighted Graph):边带有权重。
图的表示方法
-
邻接矩阵(Adjacency Matrix):
-
使用二维数组表示图。
-
适合稠密图。
-
空间复杂度:O(V2)O(V2)。
-
-
邻接表(Adjacency List):
-
使用链表或数组的数组表示图。
-
适合稀疏图。
-
空间复杂度:O(V+E)O(V+E)。
-
2. 常见图论算法
2.1 深度优先搜索(DFS)
核心思想
-
从起点出发,沿着一条路径尽可能深入地访问顶点,直到无法继续为止,然后回溯。
详细步骤
-
从起点开始,标记当前顶点为已访问。
-
遍历当前顶点的所有邻居:
-
如果邻居未被访问,则递归调用 DFS。
-
-
回溯到上一个顶点,继续遍历其他邻居。
-
重复上述步骤,直到所有顶点都被访问。
应用场景
-
图的连通性检测。
-
拓扑排序。
-
寻找强连通分量。
代码实现
void dfs(int node, vector<vector<int>>& graph, vector<bool>& visited) {
visited[node] = true;
cout << node << " "; // 输出访问的节点
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
dfs(neighbor, graph, visited);
}
}
}
// 测试代码
int main() {
vector<vector<int>> graph = {
{1, 2}, // 节点 0 的邻居
{0, 3}, // 节点 1 的邻居
{0, 3}, // 节点 2 的邻居
{1, 2} // 节点 3 的邻居
};
vector<bool> visited(graph.size(), false);
dfs(0, graph, visited); // 从节点 0 开始 DFS
return 0;
}
2.2 广度优先搜索(BFS)
核心思想
-
从起点出发,逐层访问顶点,先访问距离起点最近的顶点。
详细步骤
-
从起点开始,将其加入队列并标记为已访问。
-
从队列中取出一个顶点,访问其所有邻居:
如果邻居未被访问,则将其加入队列并标记为已访问。 -
重复上述步骤,直到队列为空。
应用场景
-
最短路径(无权图)。
-
图的连通性检测。
代码实现
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();
cout << node << " "; // 输出访问的节点
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(neighbor);
}
}
}
}
// 测试代码
int main() {
vector<vector<int>> graph = {
{1, 2}, // 节点 0 的邻居
{0, 3}, // 节点 1 的邻居
{0, 3}, // 节点 2 的邻居
{1, 2} // 节点 3 的邻居
};
bfs(0, graph); // 从节点 0 开始 BFS
return 0;
}
2.3 最短路径算法
Dijkstra 算法
-
核心思想:贪心算法,每次选择当前距离起点最近的顶点,更新其邻居的距离。
-
详细步骤
-
初始化距离数组,起点的距离为 0,其他顶点的距离为无穷大。
-
将起点加入优先队列。
-
从优先队列中取出距离最小的顶点,遍历其邻居:
-
如果通过当前顶点到达邻居的距离更短,则更新邻居的距离,并将其加入优先队列。
-
-
重复上述步骤,直到优先队列为空。
-
适用场景:带权图(权重非负)。
-
时间复杂度:O(V2)O(V2)(朴素实现),O(E+VlogV)O(E+VlogV)(优先队列优化)。
void dijkstra(int start, vector<vector<pair<int, int>>>& graph, vector<int>& dist) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
dist[start] = 0;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
for (auto& edge : graph[u]) {
int v = edge.first, w = edge.second;
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
Bellman-Ford 算法
-
核心思想:动态规划,通过松弛操作逐步更新最短路径。
-
详细步骤
-
初始化距离数组,起点的距离为 0,其他顶点的距离为无穷大。
-
进行 V−1V−1 次松弛操作:
-
遍历所有边,如果通过当前边到达终点的距离更短,则更新终点的距离。
-
-
检测负权环:
-
再次遍历所有边,如果仍能更新距离,则说明存在负权环。
-
-
适用场景:带权图(允许负权重)。
-
时间复杂度:O(V⋅E)O(V⋅E)。
bool bellmanFord(int start, vector<vector<pair<int, int>>>& graph, vector<int>& dist) {
dist[start] = 0;
for (int i = 0; i < graph.size() - 1; i++) {
for (int u = 0; u < graph.size(); u++) {
for (auto& edge : graph[u]) {
int v = edge.first, w = edge.second;
if (dist[u] != INT_MAX && dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
}
}
}
}
// 检测负权环
for (int u = 0; u < graph.size(); u++) {
for (auto& edge : graph[u]) {
int v = edge.first, w = edge.second;
if (dist[u] != INT_MAX && dist[v] > dist[u] + w) {
return false; // 存在负权环
}
}
}
return true;
}
Floyd-Warshall 算法
-
核心思想:动态规划,计算所有顶点对之间的最短路径。
-
详细步骤
-
初始化距离矩阵,对角线元素为 0,其他元素为无穷大。
-
对于每个中间顶点 kk,更新所有顶点对 (i,j)(i,j) 的距离:
-
如果通过 kk 到达 jj 的距离更短,则更新 dist[i][j]dist[i][j]。
-
-
重复上述步骤,直到所有中间顶点都被考虑。
-
适用场景:带权图(允许负权重)。
-
时间复杂度:O(V3)O(V3)。
void floydWarshall(vector<vector<int>>& dist) {
int V = dist.size();
for (int k = 0; k < V; k++) {
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
}
}
2.4 最小生成树算法
Kruskal 算法
-
核心思想:贪心算法,按权重从小到大选择边,确保不形成环。
-
详细步骤
-
将所有边按权重从小到大排序。
-
初始化并查集,每个顶点自成一个集合。
-
遍历所有边:
-
如果边的两个顶点不在同一个集合中,则将边加入最小生成树,并合并两个集合。
-
-
重复上述步骤,直到最小生成树包含 V−1V−1 条边。
-
适用场景:无向图。
-
时间复杂度:O(ElogE)O(ElogE)。
struct Edge {
int u, v, w;
bool operator<(const Edge& other) const {
return w < other.w;
}
};
int findParent(int u, vector<int>& parent) {
if (parent[u] != u) {
parent[u] = findParent(parent[u], parent);
}
return parent[u];
}
void kruskal(vector<Edge>& edges, int V) {
sort(edges.begin(), edges.end());
vector<int> parent(V);
for (int i = 0; i < V; i++) parent[i] = i;
vector<Edge> result;
for (Edge& edge : edges) {
int uRoot = findParent(edge.u, parent);
int vRoot = findParent(edge.v, parent);
if (uRoot != vRoot) {
result.push_back(edge);
parent[uRoot] = vRoot;
}
}
}
Prim 算法
-
核心思想:贪心算法,从起点开始逐步扩展最小生成树。
-
详细步骤
-
初始化优先队列,起点的权重为 0,其他顶点的权重为无穷大。
-
将起点加入优先队列。
-
从优先队列中取出权重最小的顶点,遍历其邻居:
-
如果邻居未被访问且通过当前顶点到达邻居的权重更小,则更新邻居的权重,并将其加入优先队列。
-
-
重复上述步骤,直到优先队列为空。
-
适用场景:无向图。
-
时间复杂度:O(ElogV)O(ElogV)。
void prim(int start, vector<vector<pair<int, int>>>& graph) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
vector<bool> visited(graph.size(), false);
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
int w = pq.top().first;
pq.pop();
if (visited[u]) continue;
visited[u] = true;
cout << u << " "; // 输出最小生成树的节点
for (auto& edge : graph[u]) {
int v = edge.first, weight = edge.second;
if (!visited[v]) {
pq.push({weight, v});
}
}
}
}
3. 总结
算法 | 应用场景 | 时间复杂度 |
---|---|---|
DFS | 连通性检测、拓扑排序 | O(V+E)O(V+E) |
BFS | 最短路径(无权图) | O(V+E)O(V+E) |
Dijkstra | 最短路径(非负权重) | O(E+VlogV)O(E+VlogV) |
Bellman-Ford | 最短路径(允许负权重) | O(V⋅E)O(V⋅E) |
Floyd-Warshall | 所有顶点对最短路径 | O(V3)O(V3) |
Kruskal | 最小生成树 | O(ElogE)O(ElogE) |
Prim | 最小生成树 | O(ElogV)O(ElogV) |