【轻松掌握数据结构与算法】图算法

图是一种强大的数据结构,用于表示对象之间的关系。图算法在计算机科学的许多领域都有广泛的应用,包括网络分析、路径查找和社交网络分析等。本章将详细介绍图的基本概念、表示方法、遍历算法以及一些常见的图算法。

引言

图由节点(顶点)和边组成,可以用来表示各种复杂的关系。图可以是有向的也可以是无向的,边可以有权重也可以没有权重。图的这些特性使其在解决实际问题时非常灵活。

术语解释

在图中,有一些基本的术语需要了解:

  • 顶点(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)来计算图的强连通分量。具体步骤如下:

  1. 对图进行深度优先搜索,记录每个节点的访问顺序。

  2. 反转图的边方向。

  3. 按照访问顺序的逆序对反转后的图进行深度优先搜索,每次搜索到的节点集合即为一个强连通分量。

代码示例
// 图的邻接矩阵表示
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)来计算图的割点。具体步骤如下:

  1. 对图进行深度优先搜索,记录每个节点的访问顺序和低链接值。

  2. 如果一个节点的低链接值大于其子节点的访问顺序,则该节点为割点。

代码示例
// 图的邻接矩阵表示
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)来计算图的桥。具体步骤如下:

  1. 对图进行深度优先搜索,记录每个节点的访问顺序和低链接值。

  2. 如果一个节点的低链接值大于其子节点的访问顺序,则该边为桥。

代码示例
// 图的邻接矩阵表示
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)来计算图的双连通分量。具体步骤如下:

  1. 对图进行深度优先搜索,记录每个节点的访问顺序和低链接值。

  2. 如果一个节点的低链接值大于其子节点的访问顺序,则该节点为割点。

  3. 使用栈来记录路径,每次遇到割点时,从栈中弹出路径,直到遇到割点为止,弹出的路径即为一个双连通分量。

代码示例
// 图的邻接矩阵表示
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

总结

通过以上对图算法的深入探讨,我们可以看到这些算法在计算机科学的各个领域都有着广泛的应用。无论是在算法设计、数据存储还是网络通信中,图算法都扮演着不可或缺的角色。希望这篇文章能帮助你更好地理解和应用这些强大的算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值