图算法详解:Dijkstra、Prim、Kruskal、拓扑排序和Floyd的原理与实现

Dijkstra、Prim、Kruskal和拓扑排序的原理与实现

图算法是计算机科学中的核心内容,广泛应用于网络路由、社交网络分析、任务调度等领域。本文将详细介绍四种经典图算法:Dijkstra最短路径算法、Prim最小生成树算法、Kruskal最小生成树算法和拓扑排序算法,包括它们的原理、应用场景以及完整的C语言实现。

一、Dijkstra最短路径算法

算法原理

Dijkstra算法由荷兰计算机科学家Edsger W. Dijkstra于1956年提出,用于解决单源最短路径问题。算法采用贪心策略,逐步确定从源点到所有其他顶点的最短路径。

核心思想

  1. 维护两个集合:已确定最短路径的顶点集合S和未确定的集合Q

  2. 每次从Q中选出距离源点最近的顶点u,加入S

  3. 松弛(relax)u的所有邻接顶点v的距离

  4. 重复直到Q为空

时间复杂度:使用优先队列时为O((V+E)logV),普通实现为O(V²)

应用场景

  • 网络路由协议(如OSPF)

  • 交通导航系统

  • 社交网络中查找关系最近的路径

C语言实现

#include <stdio.h>
#include <limits.h>
#include <stdbool.h>

#define V 6  // 图中顶点的数量

// 找到距离数组中最小距离的顶点的索引
int minDistance(int dist[], bool sptSet[]) {
    int min = INT_MAX, min_index;

    for (int v = 0; v < V; v++) {
        if (!sptSet[v] && dist[v] <= min) {
            min = dist[v];
            min_index = v;
        }
    }

    return min_index;
}

// 打印最短路径
void printPath(int parent[], int j) {
    // 如果j是源节点则停止递归
    if (parent[j] == -1)
        return;

    printPath(parent, parent[j]);
    printf("-> %d ", j);
}

// 打印构造的距离数组
void printSolution(int dist[], int parent[], int src) {
    printf("顶点\t 距离\t 路径");
    for (int i = 0; i < V; i++) {
        if (i != src) {
            printf("\n%d -> %d \t %d\t %d ", src, i, dist[i], src);
            printPath(parent, i);
        }
    }
    printf("\n");
}

// 实现Dijkstra算法的函数
void dijkstra(int graph[V][V], int src) {
    int dist[V];      // 保存源到i的最短距离
    bool sptSet[V];   // 如果顶点i在最短路径树中或距离已确定,则为true
    int parent[V];    // 保存最短路径树

    // 初始化所有距离为INFINITE,sptSet[]为false
    for (int i = 0; i < V; i++) {
        dist[i] = INT_MAX;
        sptSet[i] = false;
        parent[src] = -1;  // 源节点的父节点设为-1
    }

    // 源顶点到自身的距离总是0
    dist[src] = 0;

    // 找到所有顶点的最短路径
    for (int count = 0; count < V - 1; count++) {
        // 从尚未处理的顶点集中选择最小距离顶点
        int u = minDistance(dist, sptSet);

        // 标记选定的顶点为已处理
        sptSet[u] = true;

        // 更新选定顶点的相邻顶点的dist值
        for (int v = 0; v < V; v++) {
            // 更新dist[v]仅当:
            // 1. 不在sptSet中
            // 2. 存在从u到v的边
            // 3. 通过u到v的路径的总权重小于dist[v]的当前值
            if (!sptSet[v] && graph[u][v] && dist[u] != INT_MAX 
                && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
                parent[v] = u;
            }
        }
    }

    // 打印构造的距离数组和路径
    printSolution(dist, parent, src);
}

int main() {
    // 用邻接矩阵表示图
    int graph[V][V] = { {0, 4, 0, 0, 0, 0},
                        {4, 0, 8, 0, 0, 0},
                        {0, 8, 0, 7, 0, 4},
                        {0, 0, 7, 0, 9, 14},
                        {0, 0, 0, 9, 0, 10},
                        {0, 0, 4, 14, 10, 0} };

    printf("Dijkstra算法实现 - 最短路径:\n");
    dijkstra(graph, 0);  // 从顶点0开始计算最短路径

    return 0;
}

二、Prim最小生成树算法

算法原理

Prim算法用于在加权无向连通图中寻找最小生成树(MST)。它从任意顶点开始,逐步扩展树,每次添加与当前树相连且权重最小的边。

核心思想

  1. 初始化:选择任意顶点作为起始点

  2. 维护一个优先队列存储连接树与非树顶点的边

  3. 每次取出权重最小的边加入生成树

  4. 更新与新加入顶点相连的边

  5. 重复直到包含所有顶点

时间复杂度:O(ElogV)使用优先队列

应用场景

  • 网络设计(如电缆布线)

  • 集群分析

  • 图像分割

C语言实现 

#include <stdio.h>
#include <limits.h>
#include <stdbool.h>

#define V 5

int minKey(int key[], bool mstSet[]) {
    int min = INT_MAX, min_index;
    for (int v = 0; v < V; v++)
        if (!mstSet[v] && key[v] < min)
            min = key[v], min_index = v;
    return min_index;
}

void printMST(int parent[], int graph[V][V]) {
    printf("边 \t权重\n");
    for (int i = 1; i < V; i++)
        printf("%d - %d \t%d \n", parent[i], i, graph[i][parent[i]]);
}

void primMST(int graph[V][V]) {
    int parent[V];  // 存储构造的MST
    int key[V];     // 选取最小权重的边
    bool mstSet[V]; // 标记顶点是否已在MST中

    for (int i = 0; i < V; i++)
        key[i] = INT_MAX, mstSet[i] = false;

    key[0] = 0;     // 第一个顶点作为起点
    parent[0] = -1; // 根节点没有父节点

    for (int count = 0; count < V - 1; count++) {
        int u = minKey(key, mstSet);
        mstSet[u] = true;

        for (int v = 0; v < V; v++)
            if (graph[u][v] && !mstSet[v] && graph[u][v] < key[v])
                parent[v] = u, key[v] = graph[u][v];
    }

    printMST(parent, graph);
}

int main() {
    int graph[V][V] = { {0, 2, 0, 6, 0},
                        {2, 0, 3, 8, 5},
                        {0, 3, 0, 0, 7},
                        {6, 8, 0, 0, 9},
                        {0, 5, 7, 9, 0} };

    printf("Prim算法生成的最小生成树:\n");
    primMST(graph);

    return 0;
}

三、Kruskal最小生成树算法

算法原理

Kruskal算法同样用于寻找MST,但与Prim算法不同,它按照边的权重顺序依次选择,并确保不形成环。

核心思想

  1. 将所有边按权重升序排序

  2. 初始化一个空集合作为生成树

  3. 依次选择最小边,如果加入该边不会形成环,则加入生成树

  4. 使用并查集(Disjoint Set)数据结构高效检测环

时间复杂度:O(ElogE)或O(ElogV)

应用场景

  • 电路板布线

  • 网络设计

  • 近似算法的基础

C语言实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define V 5
#define E 7

struct Edge {
    int src, dest, weight;
};

struct Graph {
    int V, E;
    struct Edge* edge;
};

struct subset {
    int parent;
    int rank;
};

int find(struct subset subsets[], int i) {
    if (subsets[i].parent != i)
        subsets[i].parent = find(subsets, subsets[i].parent);
    return subsets[i].parent;
}

void Union(struct subset subsets[], int x, int y) {
    int xroot = find(subsets, x);
    int yroot = find(subsets, y);

    if (subsets[xroot].rank < subsets[yroot].rank)
        subsets[xroot].parent = yroot;
    else if (subsets[xroot].rank > subsets[yroot].rank)
        subsets[yroot].parent = xroot;
    else {
        subsets[yroot].parent = xroot;
        subsets[xroot].rank++;
    }
}

int myComp(const void* a, const void* b) {
    struct Edge* a1 = (struct Edge*)a;
    struct Edge* b1 = (struct Edge*)b;
    return a1->weight > b1->weight;
}

void KruskalMST(struct Graph* graph) {
    int V = graph->V;
    struct Edge result[V];
    int e = 0;
    int i = 0;

    qsort(graph->edge, graph->E, sizeof(graph->edge[0]), myComp);

    struct subset* subsets = (struct subset*)malloc(V * sizeof(struct subset));

    for (int v = 0; v < V; ++v) {
        subsets[v].parent = v;
        subsets[v].rank = 0;
    }

    while (e < V - 1 && i < graph->E) {
        struct Edge next_edge = graph->edge[i++];

        int x = find(subsets, next_edge.src);
        int y = find(subsets, next_edge.dest);

        if (x != y) {
            result[e++] = next_edge;
            Union(subsets, x, y);
        }
    }

    printf("Kruskal算法生成的最小生成树:\n");
    for (i = 0; i < e; ++i)
        printf("%d -- %d == %d\n", result[i].src, result[i].dest, result[i].weight);
}

int main() {
    struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
    graph->V = V;
    graph->E = E;
    graph->edge = (struct Edge*)malloc(graph->E * sizeof(struct Edge));

    // 示例图
    graph->edge[0].src = 0;
    graph->edge[0].dest = 1;
    graph->edge[0].weight = 2;

    graph->edge[1].src = 0;
    graph->edge[1].dest = 3;
    graph->edge[1].weight = 6;

    graph->edge[2].src = 1;
    graph->edge[2].dest = 2;
    graph->edge[2].weight = 3;

    graph->edge[3].src = 1;
    graph->edge[3].dest = 3;
    graph->edge[3].weight = 8;

    graph->edge[4].src = 1;
    graph->edge[4].dest = 4;
    graph->edge[4].weight = 5;

    graph->edge[5].src = 2;
    graph->edge[5].dest = 4;
    graph->edge[5].weight = 7;

    graph->edge[6].src = 3;
    graph->edge[6].dest = 4;
    graph->edge[6].weight = 9;

    KruskalMST(graph);

    return 0;
}

四、拓扑排序算法

算法原理

拓扑排序是对有向无环图(DAG)的线性排序,使得对于图中的每条有向边(u,v),u在排序中总位于v之前。

核心思想

  1. 计算所有顶点的入度

  2. 将入度为0的顶点加入队列

  3. 从队列中取出顶点并输出

  4. 将该顶点邻接顶点的入度减1,若减至0则加入队列

  5. 重复直到队列为空

时间复杂度:O(V+E)

应用场景

  • 任务调度

  • 课程安排

  • 编译器的依赖解析

C语言实现

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define V 6

struct Node {
    int vertex;
    struct Node* next;
};

struct Graph {
    struct Node** adjLists;
    int* indegree;
};

struct Node* createNode(int v) {
    struct Node* newNode = malloc(sizeof(struct Node));
    newNode->vertex = v;
    newNode->next = NULL;
    return newNode;
}

struct Graph* createGraph(int vertices) {
    struct Graph* graph = malloc(sizeof(struct Graph));
    graph->adjLists = malloc(vertices * sizeof(struct Node*));
    graph->indegree = malloc(vertices * sizeof(int));

    for (int i = 0; i < vertices; i++) {
        graph->adjLists[i] = NULL;
        graph->indegree[i] = 0;
    }

    return graph;
}

void addEdge(struct Graph* graph, int src, int dest) {
    // 添加边从src到dest
    struct Node* newNode = createNode(dest);
    newNode->next = graph->adjLists[src];
    graph->adjLists[src] = newNode;
    
    // 更新入度
    graph->indegree[dest]++;
}

void topologicalSort(struct Graph* graph) {
    int queue[V];
    int front = -1, rear = -1;
    
    // 将所有入度为0的顶点加入队列
    for (int i = 0; i < V; i++) {
        if (graph->indegree[i] == 0) {
            queue[++rear] = i;
        }
    }
    
    int count = 0;
    int topOrder[V];
    int index = 0;
    
    while (front != rear) {
        int u = queue[++front];
        topOrder[index++] = u;
        
        struct Node* temp = graph->adjLists[u];
        while (temp != NULL) {
            int v = temp->vertex;
            if (--graph->indegree[v] == 0) {
                queue[++rear] = v;
            }
            temp = temp->next;
        }
        count++;
    }
    
    if (count != V) {
        printf("图中存在环,无法进行拓扑排序\n");
        return;
    }
    
    printf("拓扑排序结果: ");
    for (int i = 0; i < V; i++)
        printf("%d ", topOrder[i]);
    printf("\n");
}

int main() {
    struct Graph* graph = createGraph(V);
    
    addEdge(graph, 5, 2);
    addEdge(graph, 5, 0);
    addEdge(graph, 4, 0);
    addEdge(graph, 4, 1);
    addEdge(graph, 2, 3);
    addEdge(graph, 3, 1);
    
    topologicalSort(graph);
    
    return 0;
}

总结比较

算法适用图类型解决的问题时间复杂度特点
Dijkstra带权有向/无向图(非负权)单源最短路径O((V+E)logV)贪心算法,使用优先队列
Prim带权无向连通图最小生成树O(ElogV)从顶点扩展,适合稠密图
Kruskal带权无向连通图最小生成树O(ElogE)按边选择,适合稀疏图
拓扑排序有向无环图(DAG)任务排序O(V+E)基于入度,检测环

每种算法都有其特定的应用场景和优势,理解它们的原理和实现对于解决实际问题至关重要。在实际应用中,应根据具体问题的特点选择合适的算法。

Floyd-Warshall算法:全源最短路径的经典解法

算法原理

Floyd-Warshall算法是解决全源最短路径问题的动态规划算法,可以处理包含负权边(但不含负权回路)的图。该算法由Robert Floyd和Stephen Warshall在1962年分别独立提出。

核心思想

  1. 使用动态规划逐步改善最短路径估计

  2. 定义dᵏᵢⱼ为从i到j且中间顶点编号不超过k的最短路径长度

  3. 通过三重循环逐步放松所有可能的路径

状态转移方程
dᵏᵢⱼ = min(dᵏ⁻¹ᵢⱼ, dᵏ⁻¹ᵢₖ + dᵏ⁻¹ₖⱼ)

时间复杂度:O(V³) —— 非常适合稠密图
空间复杂度:O(V²)

算法特点

  • 优点

    • 代码实现简单

    • 可以直接处理负权边(只要没有负权回路)

    • 一次计算即可获得所有顶点对的最短路径

  • 缺点

    • 时间复杂度较高,不适合大规模稀疏图

    • 不能处理包含负权回路的图

应用场景

  • 网络路由优化

  • 交通网络分析

  • 社交网络中的关系距离计算

  • 任意两点间最短路径的预计算

C语言实现

#include <stdio.h>

#define V 4
#define INF 99999

void printSolution(int dist[][V]);

void floydWarshall(int graph[][V]) {
    int dist[V][V], i, j, k;

    // 初始化距离矩阵
    for (i = 0; i < V; i++)
        for (j = 0; j < V; j++)
            dist[i][j] = graph[i][j];

    // 逐步放松所有顶点作为中间点
    for (k = 0; k < V; k++) {
        for (i = 0; i < V; i++) {
            for (j = 0; j < V; j++) {
                // 如果通过k顶点路径更短,则更新
                if (dist[i][k] + dist[k][j] < dist[i][j])
                    dist[i][j] = dist[i][k] + dist[k][j];
            }
        }
    }

    // 打印最终的最短距离矩阵
    printSolution(dist);
}

void printSolution(int dist[][V]) {
    printf("Floyd-Warshall算法计算的全源最短路径矩阵:\n");
    for (int i = 0; i < V; i++) {
        for (int j = 0; j < V; j++) {
            if (dist[i][j] == INF)
                printf("%7s", "INF");
            else
                printf("%7d", dist[i][j]);
        }
        printf("\n");
    }
}

int main() {
    /* 示例图
            10
       (0)------->(3)
        |         /|\
      5 |          |
        |          | 1
       \|/         |
       (1)------->(2)
            3           */
    int graph[V][V] = { {0,   5,  INF, 10},
                        {INF, 0,   3, INF},
                        {INF, INF, 0,   1},
                        {INF, INF, INF, 0} };

    floydWarshall(graph);
    return 0;
}

算法执行过程解析

  1. 初始化阶段

    • 创建距离矩阵dist[][],初始值与图的邻接矩阵相同

    • 对角线元素设为0(顶点到自身的距离)

    • 不直接相连的顶点距离设为INF

  2. 动态规划阶段

    • 外层循环(k):依次考虑每个顶点作为中间点

    • 中层循环(i):遍历所有起点

    • 内层循环(j):遍历所有终点

    • 检查是否通过顶点k可以缩短i到j的路径

  3. 结果输出

    • 最终dist[i][j]包含从i到j的最短路径距离

    • 可以额外维护一个路径矩阵来记录具体路径

路径重建的实现

如果需要记录具体路径而不仅仅是距离,可以扩展算法如下:

void floydWarshallWithPath(int graph[][V]) {
    int dist[V][V], next[V][V], i, j, k;

    // 初始化
    for (i = 0; i < V; i++) {
        for (j = 0; j < V; j++) {
            dist[i][j] = graph[i][j];
            if (graph[i][j] != INF && i != j)
                next[i][j] = j;
            else
                next[i][j] = -1;
        }
    }

    // 算法主循环
    for (k = 0; k < V; k++) {
        for (i = 0; i < V; i++) {
            for (j = 0; j < V; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    next[i][j] = next[i][k];
                }
            }
        }
    }

    // 打印结果和路径
    printf("最短路径距离矩阵:\n");
    printSolution(dist);
    
    printf("\n路径重建示例:\n");
    for (i = 0; i < V; i++) {
        for (j = 0; j < V; j++) {
            if (i != j && next[i][j] != -1) {
                printf("从 %d 到 %d 的路径: %d", i, j, i);
                int u = i, v = j;
                while (u != v) {
                    u = next[u][v];
                    printf(" -> %d", u);
                }
                printf(",总距离: %d\n", dist[i][j]);
            }
        }
    }
}

算法优化与变种

  1. 空间优化:可以使用单个距离矩阵,原地更新

  2. 提前终止:检测到负权回路时可以提前终止

  3. 并行化:由于三重循环的独立性,适合并行计算

  4. 稀疏图优化:对于稀疏图可以结合Dijkstra算法改进

与其他算法的比较

特性Floyd-WarshallDijkstra(所有顶点)Johnson算法
时间复杂度O(V³)O(V(E+VlogV))O(VE+V²logV)
负权边支持不支持支持
负权回路可检测不可检测可检测
实现复杂度简单中等较复杂
适用场景稠密图稀疏图稀疏图+负权

实际应用案例

  1. 交通网络分析:计算城市所有地点之间的最短行车路线

  2. 网络路由协议:某些路由协议中预计算所有节点间的最短路径

  3. 社交网络:计算社交图中任意两人之间的"关系距离"

  4. 游戏开发:在战略游戏中预计算地图上所有位置间的移动成本

完整测试用例

#include <stdio.h>

#define V 5
#define INF 99999

void printSolution(int dist[][V]);

void floydWarshall(int graph[][V]) {
    /* 实现内容同上 */
}

int main() {
    // 测试用例1:常规图
    int graph1[V][V] = { {0,   3,  8, INF, -4},
                         {INF, 0, INF,  1,  7},
                         {INF, 4,   0, INF, INF},
                         {2, INF, -5,   0, INF},
                         {INF, INF, INF, 6, 0} };
    
    printf("测试用例1:\n");
    floydWarshall(graph1);
    
    // 测试用例2:包含负权边但不含负权回路
    int graph2[V][V] = { {0, INF, -2, INF, INF},
                         {4,   0,  3, INF, INF},
                         {INF, INF, 0,   2, INF},
                         {INF, -1, INF, 0, INF},
                         {INF, INF, INF, 1, 0} };
    
    printf("\n测试用例2:\n");
    floydWarshall(graph2);
    
    // 测试用例3:完全连通图
    int graph3[V][V] = { {0, 2, 2, 2, 2},
                         {2, 0, 2, 2, 2},
                         {2, 2, 0, 2, 2},
                         {2, 2, 2, 0, 2},
                         {2, 2, 2, 2, 0} };
    
    printf("\n测试用例3:\n");
    floydWarshall(graph3);
    
    return 0;
}

Floyd-Warshall算法以其简洁的实现和强大的功能,成为图算法中不可或缺的工具。虽然时间复杂度较高,但对于中等规模的图或需要全源最短路径的场景,它仍然是首选算法。理解并掌握这一算法,对于解决复杂的网络优化问题具有重要意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值