图是一种强大的数据结构,用于表示对象之间的关系。图算法在计算机科学的许多领域都有广泛的应用,包括网络分析、路径查找和社交网络分析等。本章将详细介绍图的基本概念、表示方法、遍历算法以及一些常见的图算法。
引言
图由节点(顶点)和边组成,可以用来表示各种复杂的关系。图可以是有向的也可以是无向的,边可以有权重也可以没有权重。图的这些特性使其在解决实际问题时非常灵活。
术语解释
在图中,有一些基本的术语需要了解:
-
顶点(Vertex):图中的基本单元。
-
边(Edge):连接两个顶点的线。
-
度(Degree):与一个顶点相连的边的数量。
-
路径(Path):从一个顶点到另一个顶点的边的序列。
-
环(Cycle):起点和终点相同的路径。
-
连通图(Connected Graph):图中任意两个顶点都是连通的。
图的应用
图在许多领域都有应用,包括:
-
社交网络分析:分析人与人之间的关系。
-
网络路由:计算数据包在网络中的传输路径。
-
项目管理:使用图来表示任务之间的依赖关系。
图的表示
图可以用多种方式表示,最常见的是邻接矩阵和邻接表。
邻接矩阵
邻接矩阵是一个二维数组,用于表示图中顶点之间的连接关系。如果顶点 i 和顶点 j 之间有边,则矩阵的第 i 行第 j 列的值为 1(或边的权重),否则为 0。
邻接表
邻接表是一个数组,每个元素是一个链表,用于存储与该顶点相连的所有顶点。
代码示例
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int vertex;
struct Node* next;
} Node;
typedef struct Graph {
int numVertices;
Node** adjLists;
} Graph;
// 创建图
Graph* createGraph(int vertices) {
Graph* graph = malloc(sizeof(Graph));
graph->numVertices = vertices;
graph->adjLists = malloc(vertices * sizeof(Node*));
int i;
for (i = 0; i < vertices; i++) {
graph->adjLists[i] = NULL;
}
return graph;
}
// 添加边
void addEdge(Graph* graph, int src, int dest) {
// 添加从 src 到 dest 的边
Node* newNode = malloc(sizeof(Node));
newNode->vertex = dest;
newNode->next = graph->adjLists[src];
graph->adjLists[src] = newNode;
// 添加从 dest 到 src 的边(如果是无向图)
newNode = malloc(sizeof(Node));
newNode->vertex = src;
newNode->next = graph->adjLists[dest];
graph->adjLists[dest] = newNode;
}
// 打印图
void printGraph(Graph* graph) {
for (int v = 0; v < graph->numVertices; v++) {
Node* temp = graph->adjLists[v];
printf("\n Adjacency list of vertex %d\n ", v);
while (temp) {
printf("%d -> ", temp->vertex);
temp = temp->next;
}
printf("\n");
}
}
int main() {
Graph* graph = createGraph(4);
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 2);
addEdge(graph, 2, 3);
printGraph(graph);
return 0;
}
输入输出示例
Adjacency list of vertex 0
1 -> 2 ->
Adjacency list of vertex 1
0 -> 2 ->
Adjacency list of vertex 2
0 -> 1 -> 3 ->
Adjacency list of vertex 3
2 ->
图的遍历
图的遍历是按照特定的顺序访问图中的每个顶点。常见的遍历方式有深度优先搜索(DFS)和广度优先搜索(BFS)。
深度优先搜索(DFS)
深度优先搜索从一个顶点开始,尽可能深地搜索图的分支。
广度优先搜索(BFS)
广度优先搜索从一个顶点开始,逐层访问图中的顶点。
代码示例
void DFSUtil(Graph* graph, int v, int visited[]) {
visited[v] = 1;
printf("%d ", v);
Node* temp = graph->adjLists[v];
while (temp) {
int adjVertex = temp->vertex;
if (visited[adjVertex] == 0) {
DFSUtil(graph, adjVertex, visited);
}
temp = temp->next;
}
}
void DFS(Graph* graph, int startVertex) {
int visited[graph->numVertices];
for (int i = 0; i < graph->numVertices; i++) {
visited[i] = 0;
}
DFSUtil(graph, startVertex, visited);
}
void BFS(Graph* graph, int startVertex) {
int visited[graph->numVertices];
for (int i = 0; i < graph->numVertices; i++) {
visited[i] = 0;
}
Queue* queue = createQueue();
visited[startVertex] = 1;
enqueue(queue, startVertex);
while (!isEmpty(queue)) {
int currVertex = dequeue(queue);
printf("%d ", currVertex);
Node* temp = graph->adjLists[currVertex];
while (temp) {
int adjVertex = temp->vertex;
if (visited[adjVertex] == 0) {
visited[adjVertex] = 1;
enqueue(queue, adjVertex);
}
temp = temp->next;
}
}
}
输入输出示例
假设我们有以下图:
0 -- 1
| / \
| / 2
|/ \
3--------4
-
DFS 从顶点 0 开始:0 1 3 4 2
-
BFS 从顶点 0 开始:0 1 3 2 4
拓扑排序
拓扑排序是对有向无环图(DAG)的顶点进行线性排序,使得对于图中的每一条边 (u, v),u 都在 v 之前。
代码示例
void topologicalSortUtil(Graph* graph, int v, int visited[], Stack* stack) {
visited[v] = 1;
Node* temp = graph->adjLists[v];
while (temp) {
int adjVertex = temp->vertex;
if (!visited[adjVertex]) {
topologicalSortUtil(graph, adjVertex, visited, stack);
}
temp = temp->next;
}
push(stack, v);
}
void topologicalSort(Graph* graph) {
Stack* stack = createStack();
int visited[graph->numVertices];
for (int i = 0; i < graph->numVertices; i++) {
visited[i] = 0;
}
for (int i = 0; i < graph->numVertices; i++) {
if (!visited[i]) {
topologicalSortUtil(graph, i, visited, stack);
}
}
while (!isEmpty(stack)) {
printf("%d ", pop(stack));
}
}
输入输出示例
假设我们有以下有向无环图:
5 --> 2 --> 3
| / \
| / 1
| / \
| / 0
| / \
| / 4
| / \
| / 6
| / \
7---------------------8
拓扑排序结果:7 5 2 3 1 0 4 6 8
最短路径算法
最短路径算法用于计算图中两个顶点之间的最短路径。常见的最短路径算法包括 Dijkstra 算法和 Bellman-Ford 算法。
Dijkstra 算法
Dijkstra 算法用于计算单源最短路径,适用于无负权边的图。
Bellman-Ford 算法
Bellman-Ford 算法用于计算单源最短路径,适用于有负权边的图。
代码示例
void dijkstra(Graph* graph, int src) {
int dist[graph->numVertices];
int visited[graph->numVertices];
for (int i = 0; i < graph->numVertices; i++) {
dist[i] = INT_MAX;
visited[i] = 0;
}
dist[src] = 0;
for (int count = 0; count < graph->numVertices - 1; count++) {
int u = minDistance(dist, visited, graph->numVertices);
visited[u] = 1;
Node* temp = graph->adjLists[u];
while (temp) {
int adjVertex = temp->vertex;
int weight = temp->weight;
if (!visited[adjVertex] && dist[u] != INT_MAX && weight + dist[u] < dist[adjVertex]) {
dist[adjVertex] = weight + dist[u];
}
temp = temp->next;
}
}
printSolution(dist, graph->numVertices);
}
void bellmanFord(Graph* graph, int src) {
int dist[graph->numVertices];
for (int i = 0; i < graph->numVertices; i++) {
dist[i] = INT_MAX;
}
dist[src] = 0;
for (int i = 1; i <= graph->numVertices - 1; i++) {
for (int j = 0; j < graph->numEdges; j++) {
int u = graph->edge[j].src;
int v = graph->edge[j].dest;
int weight = graph->edge[j].weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
for (int i = 0; i < graph->numEdges; i++) {
int u = graph->edge[i].src;
int v = graph->edge[i].dest;
int weight = graph->edge[i].weight;
if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
printf("Graph contains negative weight cycle");
return;
}
}
printSolution(dist, graph->numVertices);
}
输入输出示例
假设我们有以下图:
0 --(1)-- 1 --(2)-- 2
| / \ |
(4) (3) (5) (1)
| / \ |
| / \ |
| / \ |
| / \ |
| / \ |
| / \|
| / 3
|/ (5)
4------------------(3)
-
Dijkstra 算法从顶点 0 开始:
-
0 到 0 的最短路径:0
-
0 到 1 的最短路径:1
-
0 到 2 的最短路径:3
-
0 到 3 的最短路径:6
-
0 到 4 的最短路径:7
-
-
Bellman-Ford 算法从顶点 0 开始:
-
0 到 0 的最短路径:0
-
0 到 1 的最短路径:1
-
0 到 2 的最短路径:3
-
0 到 3 的最短路径:6
-
0 到 4 的最短路径:7
-
最小生成树
最小生成树是图的一个子图,包含图中的所有顶点,且边的总权重最小。常见的最小生成树算法包括 Kruskal 算法和 Prim 算法。
Kruskal 算法
Kruskal 算法通过选择最小权重的边来构建最小生成树,适用于稀疏图。
Prim 算法
Prim 算法通过选择最小权重的边来构建最小生成树,适用于稠密图。
代码示例
typedef struct Edge {
int src, dest, weight;
} Edge;
typedef struct Graph {
int V, E;
Edge* edge;
} Graph;
// 创建图
Graph* createGraph(int V, int E) {
Graph* graph = malloc(sizeof(Graph));
graph->V = V;
graph->E = E;
graph->edge = malloc(E * sizeof(Edge));
return graph;
}
// 查找函数
int find(int parent[], int i) {
if (parent[i] == i)
return i;
return find(parent, parent[i]);
}
// 合并函数
void unionSets(int parent[], int x, int y) {
x = find(parent, x);
y = find(parent, y);
parent[x] = y;
}
// Kruskal 算法
void kruskalMST(Graph* graph) {
int V = graph->V;
Edge result[V]; // 存储结果
int e = 0; // 结果中的边的数量
int i = 0; // 排序后的边的索引
qsort(graph->edge, graph->E, sizeof(graph->edge[0]), compare);
int parent[V];
for (int v = 0; v < V; ++v)
parent[v] = v;
while (e < V - 1) {
Edge next_edge = graph->edge[i++];
int x = find(parent, next_edge.src);
int y = find(parent, next_edge.dest);
if (x != y) {
result[e++] = next_edge;
unionSets(parent, x, y);
}
}
printf("Following are the edges in the constructed MST\n");
for (i = 0; i < e; ++i)
printf("%d -- %d == %d\n", result[i].src, result[i].dest, result[i].weight);
}
// Prim 算法
void primMST(Graph* graph) {
int parent[graph->V];
int key[graph->V];
int mstSet[graph->V];
for (int i = 0; i < graph->V; i++)
key[i] = INT_MAX, mstSet[i] = 0;
key[0] = 0;
parent[0] = -1;
for (int count = 0; count < graph->V - 1; count++) {
int u = minKey(key, mstSet, graph->V);
mstSet[u] = 1;
for (int v = 0; v < graph->V; v++)
if (graph->adjMatrix[u][v] && mstSet[v] == 0 && graph->adjMatrix[u][v] < key[v])
parent[v] = u, key[v] = graph->adjMatrix[u][v];
}
printf("Edge \tWeight\n");
for (int i = 1; i < graph->V; i++)
printf("%d - %d \t%d \n", parent[i], i, graph->adjMatrix[i][parent[i]]);
}
输入输出示例
假设我们有以下图:
0 --(1)-- 1 --(2)-- 2
| / \ |
(4) (3) (5) (1)
| / \ |
| / \ |
| / \ |
| / \ |
| / \ |
| / \|
| / 3
|/ (5)
4------------------(3)
-
Kruskal 算法:
-
0 - 1 == 1
-
1 - 2 == 2
-
0 - 4 == 4
-
3 - 4 == 3
-
2 - 3 == 1
-
-
Prim 算法:
-
Edge Weight
-
0 - 1 1
-
1 - 2 2
-
0 - 4 4
-
3 - 4 3
-
2 - 3 1
-
图算法的问题与解决方案
问题 1:计算图的强连通分量
解决方案:使用深度优先搜索(DFS)来计算图的强连通分量。具体步骤如下:
-
对图进行深度优先搜索,记录每个节点的访问顺序。
-
反转图的边方向。
-
按照访问顺序的逆序对反转后的图进行深度优先搜索,每次搜索到的节点集合即为一个强连通分量。
代码示例
// 图的邻接矩阵表示
int adjMatrix[256][256];
int table[256];
int dfsnum[256], num = 0, low[256];
int postOrder[256];
int postOrderIndex = 0;
// 深度优先搜索
void dfs(int u) {
low[u] = dfsnum[u] = num++;
table[u] = 1;
for (int v = 0; v < 256; v++) {
if (adjMatrix[u][v] && table[v] == -1) {
if (dfsnum[v] == -1) {
dfs(v);
}
low[u] = fmin(low[u], low[v]);
}
}
postOrder[postOrderIndex++] = u;
}
// 计算强连通分量
void stronglyConnectedComponents(int n) {
memset(table, -1, sizeof(table));
memset(dfsnum, -1, sizeof(dfsnum));
memset(low, -1, sizeof(low));
for (int i = 0; i < n; i++) {
if (table[i] == -1) {
dfs(i);
}
}
// 反转图
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
adjMatrix[i][j] = !adjMatrix[i][j];
}
}
memset(table, -1, sizeof(table));
for (int i = postOrderIndex - 1; i >= 0; i--) {
if (table[postOrder[i]] == -1) {
dfs(postOrder[i]);
// 输出强连通分量
for (int j = 0; j < n; j++) {
if (table[j] == 1) {
printf("%d ", j);
}
}
printf("\n");
}
}
}
输入输出示例
假设我们有以下图:
0 -> 1
| |
v v
2 <- 3
强连通分量:
-
0 1 2 3
问题 2:计算图的割点
解决方案:使用深度优先搜索(DFS)来计算图的割点。具体步骤如下:
-
对图进行深度优先搜索,记录每个节点的访问顺序和低链接值。
-
如果一个节点的低链接值大于其子节点的访问顺序,则该节点为割点。
代码示例
// 图的邻接矩阵表示
int adjMatrix[256][256];
int dfsnum[256], num = 0, low[256];
int parent[256];
int isArticulation[256];
// 深度优先搜索
void dfs(int u, int p) {
dfsnum[u] = low[u] = num++;
parent[u] = p;
int children = 0;
for (int v = 0; v < 256; v++) {
if (adjMatrix[u][v] && v != p) {
if (dfsnum[v] == -1) {
dfs(v, u);
low[u] = fmin(low[u], low[v]);
if (low[v] >= dfsnum[u] && p != -1) {
isArticulation[u] = 1;
}
children++;
} else {
low[u] = fmin(low[u], dfsnum[v]);
}
}
}
if (p == -1 && children > 1) {
isArticulation[u] = 1;
}
}
// 计算割点
void findArticulationPoints(int n) {
memset(dfsnum, -1, sizeof(dfsnum));
memset(low, -1, sizeof(low));
memset(parent, -1, sizeof(parent));
memset(isArticulation, 0, sizeof(isArticulation));
num = 0;
for (int i = 0; i < n; i++) {
if (dfsnum[i] == -1) {
dfs(i, -1);
}
}
for (int i = 0; i < n; i++) {
if (isArticulation[i]) {
printf("%d ", i);
}
}
printf("\n");
}
输入输出示例
假设我们有以下图:
0 -- 1
| / \
| / 2
|/ \
3--------4
割点:1 2
问题 3:计算图的桥
解决方案:使用深度优先搜索(DFS)来计算图的桥。具体步骤如下:
-
对图进行深度优先搜索,记录每个节点的访问顺序和低链接值。
-
如果一个节点的低链接值大于其子节点的访问顺序,则该边为桥。
代码示例
// 图的邻接矩阵表示
int adjMatrix[256][256];
int dfsnum[256], num = 0, low[256];
int parent[256];
int isBridge[256][256];
// 深度优先搜索
void dfs(int u, int p) {
dfsnum[u] = low[u] = num++;
parent[u] = p;
for (int v = 0; v < 256; v++) {
if (adjMatrix[u][v] && v != p) {
if (dfsnum[v] == -1) {
dfs(v, u);
low[u] = fmin(low[u], low[v]);
if (low[v] > dfsnum[u]) {
isBridge[u][v] = 1;
isBridge[v][u] = 1;
}
} else {
low[u] = fmin(low[u], dfsnum[v]);
}
}
}
}
// 计算桥
void findBridges(int n) {
memset(dfsnum, -1, sizeof(dfsnum));
memset(low, -1, sizeof(low));
memset(parent, -1, sizeof(parent));
memset(isBridge, 0, sizeof(isBridge));
num = 0;
for (int i = 0; i < n; i++) {
if (dfsnum[i] == -1) {
dfs(i, -1);
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (isBridge[i][j]) {
printf("(%d, %d) ", i, j);
}
}
}
printf("\n");
}
输入输出示例
假设我们有以下图:
0 -- 1
| / \
| / 2
|/ \
3--------4
桥:(0, 1) (1, 2) (3, 4)
问题 4:计算图的双连通分量
解决方案:使用深度优先搜索(DFS)来计算图的双连通分量。具体步骤如下:
-
对图进行深度优先搜索,记录每个节点的访问顺序和低链接值。
-
如果一个节点的低链接值大于其子节点的访问顺序,则该节点为割点。
-
使用栈来记录路径,每次遇到割点时,从栈中弹出路径,直到遇到割点为止,弹出的路径即为一个双连通分量。
代码示例
// 图的邻接矩阵表示
int adjMatrix[256][256];
int dfsnum[256], num = 0, low[256];
int parent[256];
int isArticulation[256];
int stack[256], top = 0;
// 深度优先搜索
void dfs(int u, int p) {
dfsnum[u] = low[u] = num++;
parent[u] = p;
int children = 0;
for (int v = 0; v < 256; v++) {
if (adjMatrix[u][v] && v != p) {
if (dfsnum[v] == -1) {
stack[top++] = v;
dfs(v, u);
low[u] = fmin(low[u], low[v]);
if (low[v] >= dfsnum[u] && p != -1) {
isArticulation[u] = 1;
}
children++;
} else {
low[u] = fmin(low[u], dfsnum[v]);
}
}
}
if (p == -1 && children > 1) {
isArticulation[u] = 1;
}
if (isArticulation[u] || (p != -1 && low[u] == dfsnum[u])) {
printf("Biconnected component:\n");
while (stack[top - 1] != u) {
printf("%d ", stack[--top]);
}
printf("%d\n", u);
}
}
// 计算双连通分量
void findBiconnectedComponents(int n) {
memset(dfsnum, -1, sizeof(dfsnum));
memset(low, -1, sizeof(low));
memset(parent, -1, sizeof(parent));
memset(isArticulation, 0, sizeof(isArticulation));
num = 0;
top = 0;
for (int i = 0; i < n; i++) {
if (dfsnum[i] == -1) {
dfs(i, -1);
}
}
}
输入输出示例
假设我们有以下图:
0 -- 1
| / \
| / 2
|/ \
3--------4
双连通分量:
-
0 1 3
-
1 2 4
总结
通过以上对图算法的深入探讨,我们可以看到这些算法在计算机科学的各个领域都有着广泛的应用。无论是在算法设计、数据存储还是网络通信中,图算法都扮演着不可或缺的角色。希望这篇文章能帮助你更好地理解和应用这些强大的算法。