最小生成树和最短路径

最小生成树(Minimum Spanning Tree,MST)是一个无向加权连通图的子图,它包含图中所有顶点,并且所有边的权值之和最小。常见的求解最小生成树的算法有克鲁斯卡尔(Kruskal)算法和普里姆(Prim)算法,下面为你详细介绍这两种算法。

克鲁斯卡尔(Kruskal)算法

算法思路

Kruskal 算法基于贪心策略,其核心思想是先将图中所有边按照权值从小到大进行排序,然后依次选取权值最小的边,如果这条边加入到当前的生成树中不会形成环,就将其加入;重复这个过程,直到生成树包含图中的所有顶点。

算法步骤

  1. 边排序:将图中所有边按照权值从小到大进行排序。
  2. 初始化并查集:将每个顶点看作一个独立的集合,即每个顶点的父节点是它自己。
  3. 遍历边:依次选取权值最小的边,检查这条边的两个端点是否属于不同的集合。如果是,则将这条边加入生成树,并合并这两个集合;否则,跳过这条边。
  4. 终止条件:当生成树中包含的边数等于顶点数减 1 时,算法结束。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>

#define MAX_EDGES 10000
#define MAX_VERTICES 1000

// 边的结构体
typedef struct {
    int u, v;  // 边的两个端点
    int weight;  // 边的权值
} Edge;

// 比较函数,用于 qsort 对边按权值排序
int compare(const void *a, const void *b) {
    Edge *edgeA = (Edge *)a;
    Edge *edgeB = (Edge *)b;
    return edgeA->weight - edgeB->weight;
}

// 并查集查找函数,带路径压缩
int find(int parent[], int i) {
    if (parent[i] == i)
        return i;
    return parent[i] = find(parent, parent[i]);
}

// 并查集合并函数
void unionSets(int parent[], int x, int y) {
    int xroot = find(parent, x);
    int yroot = find(parent, y);
    parent[xroot] = yroot;
}

// Kruskal 算法
int kruskal(Edge edges[], int numEdges, int numVertices) {
    qsort(edges, numEdges, sizeof(Edge), compare);  // 对边按权值排序

    int *parent = (int *)malloc(numVertices * sizeof(int));
    for (int i = 0; i < numVertices; i++) {
        parent[i] = i;  // 初始化并查集
    }

    int mstWeight = 0;  // 最小生成树的总权值
    int edgeCount = 0;  // 已加入最小生成树的边数

    for (int i = 0; i < numEdges && edgeCount < numVertices - 1; i++) {
        int u = edges[i].u;
        int v = edges[i].v;
        int weight = edges[i].weight;

        int setU = find(parent, u);
        int setV = find(parent, v);

        if (setU != setV) {
            mstWeight += weight;
            edgeCount++;
            unionSets(parent, setU, setV);
        }
    }

    free(parent);
    return mstWeight;
}

int main() {
    int numVertices = 4;
    int numEdges = 5;
    Edge edges[MAX_EDGES] = {
        {0, 1, 10},
        {0, 2, 6},
        {0, 3, 5},
        {1, 3, 15},
        {2, 3, 4}
    };

    int mstWeight = kruskal(edges, numEdges, numVertices);
    printf("最小生成树的总权值: %d\n", mstWeight);

    return 0;
}

普里姆(Prim)算法

算法思路

Prim 算法同样基于贪心策略,它从一个任意顶点开始,逐步扩展生成树,每次选择与当前生成树相连的权值最小的边,将其另一端的顶点加入生成树,直到生成树包含图中的所有顶点。

算法步骤

  1. 初始化:选择一个任意顶点作为起始顶点,将其加入生成树。
  2. 扩展生成树:每次从与当前生成树相连的边中选择权值最小的边,将其另一端的顶点加入生成树。
  3. 重复步骤 2:直到生成树包含图中的所有顶点。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 1000

// 找到距离生成树最近的顶点
int minKey(int key[], int mstSet[], int numVertices) {
    int min = INT_MAX, min_index;

    for (int v = 0; v < numVertices; v++) {
        if (mstSet[v] == 0 && key[v] < min) {
            min = key[v];
            min_index = v;
        }
    }

    return min_index;
}

// Prim 算法
int prim(int graph[MAX_VERTICES][MAX_VERTICES], int numVertices) {
    int key[MAX_VERTICES];  // 存储每个顶点到生成树的最小权值
    int mstSet[MAX_VERTICES];  // 标记顶点是否已加入生成树
    int parent[MAX_VERTICES];  // 存储每个顶点在生成树中的父节点

    // 初始化
    for (int i = 0; i < numVertices; i++) {
        key[i] = INT_MAX;
        mstSet[i] = 0;
    }

    key[0] = 0;  // 从顶点 0 开始
    parent[0] = -1;  // 顶点 0 没有父节点

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

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

    int mstWeight = 0;
    for (int i = 1; i < numVertices; i++) {
        mstWeight += graph[i][parent[i]];
    }

    return mstWeight;
}

int main() {
    int numVertices = 4;
    int graph[MAX_VERTICES][MAX_VERTICES] = {
        {0, 10, 6, 5},
        {10, 0, 0, 15},
        {6, 0, 0, 4},
        {5, 15, 4, 0}
    };

    int mstWeight = prim(graph, numVertices);
    printf("最小生成树的总权值: %d\n", mstWeight);

    return 0;
}

两种算法的比较

  • 适用场景:Kruskal 算法适用于边稀疏的图,因为它主要对边进行操作;Prim 算法适用于边稠密的图,因为它主要对顶点进行操作。
  • 实现难度:Kruskal 算法需要使用并查集,实现相对复杂一些;Prim 算法的实现相对简单。

最短路径问题是图论中的经典问题,旨在寻找图中两个顶点之间的最短路径。下面为你详细介绍几种常见的最短路径算法:

1. 迪杰斯特拉(Dijkstra)算法

算法思路

Dijkstra 算法用于求解单源最短路径问题,即从一个给定的源顶点到图中其他所有顶点的最短路径。该算法基于贪心策略,每次从未确定最短路径的顶点中选择距离源顶点最近的顶点,然后以该顶点为中间点,更新其他顶点到源顶点的距离,直到所有顶点的最短路径都被确定。

算法步骤

  1. 初始化:将源顶点的距离设为 0,其他顶点的距离设为无穷大;标记所有顶点为未确定最短路径。
  2. 选择顶点:从未确定最短路径的顶点中选择距离源顶点最近的顶点。
  3. 更新距离:以该顶点为中间点,更新其相邻顶点到源顶点的距离。
  4. 标记顶点:标记该顶点为已确定最短路径。
  5. 重复步骤 2 - 4:直到所有顶点的最短路径都被确定。
代码实现(C 语言)

 

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

#define MAX_VERTICES 100

// 找到距离源顶点最近且未确定最短路径的顶点
int minDistance(int dist[], int sptSet[], int numVertices) {
    int min = INT_MAX, min_index;

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

    return min_index;
}

// 迪杰斯特拉算法
void dijkstra(int graph[MAX_VERTICES][MAX_VERTICES], int numVertices, int src) {
    int dist[MAX_VERTICES];  // 存储每个顶点到源顶点的最短距离
    int sptSet[MAX_VERTICES];  // 标记顶点是否已确定最短路径

    // 初始化
    for (int i = 0; i < numVertices; i++) {
        dist[i] = INT_MAX;
        sptSet[i] = 0;
    }

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

    for (int count = 0; count < numVertices - 1; count++) {
        int u = minDistance(dist, sptSet, numVertices);
        sptSet[u] = 1;

        for (int v = 0; v < numVertices; 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];
            }
        }
    }

    // 输出结果
    printf("顶点到源顶点的最短距离:\n");
    for (int i = 0; i < numVertices; i++) {
        printf("顶点 %d: %d\n", i, dist[i]);
    }
}

int main() {
    int numVertices = 5;
    int graph[MAX_VERTICES][MAX_VERTICES] = {
        {0, 4, 0, 0, 0},
        {4, 0, 8, 0, 0},
        {0, 8, 0, 7, 0},
        {0, 0, 7, 0, 9},
        {0, 0, 0, 9, 0}
    };

    int src = 0;
    dijkstra(graph, numVertices, src);

    return 0;
}

 

 

2. 贝尔曼 - 福特(Bellman - Ford)算法

算法思路

Bellman - Ford 算法同样用于求解单源最短路径问题,但它可以处理带有负权边的图。该算法通过对所有边进行  次松弛操作,逐步更新每个顶点到源顶点的最短距离,最后再进行一次松弛操作,检查是否存在负权回路。

算法步骤

  1. 初始化:将源顶点的距离设为 0,其他顶点的距离设为无穷大。
  2. 松弛操作:对所有边进行V-1 次松弛操作,即对于每条边(u,v),如果 dist[u]+weight(u,v)<dist[v],则更新 dist[v]为dist[u]+weight(u,v) 。
  3. 检查负权回路:再进行一次松弛操作,如果还能更新某个顶点的距离,则说明图中存在负权回路。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 100
#define MAX_EDGES 100

// 边的结构体
typedef struct {
    int src, dest, weight;
} Edge;

// 贝尔曼 - 福特算法
int bellmanFord(Edge edges[], int numVertices, int numEdges, int src) {
    int dist[MAX_VERTICES];

    // 初始化
    for (int i = 0; i < numVertices; i++) {
        dist[i] = INT_MAX;
    }
    dist[src] = 0;

    // 进行 V - 1 次松弛操作
    for (int i = 0; i < numVertices - 1; i++) {
        for (int j = 0; j < numEdges; j++) {
            int u = edges[j].src;
            int v = edges[j].dest;
            int weight = edges[j].weight;

            if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
                dist[v] = dist[u] + weight;
            }
        }
    }

    // 检查负权回路
    for (int j = 0; j < numEdges; j++) {
        int u = edges[j].src;
        int v = edges[j].dest;
        int weight = edges[j].weight;

        if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
            printf("图中存在负权回路\n");
            return 0;
        }
    }

    // 输出结果
    printf("顶点到源顶点的最短距离:\n");
    for (int i = 0; i < numVertices; i++) {
        printf("顶点 %d: %d\n", i, dist[i]);
    }

    return 1;
}

int main() {
    int numVertices = 5;
    int numEdges = 8;
    Edge edges[MAX_EDGES] = {
        {0, 1, -1},
        {0, 2, 4},
        {1, 2, 3},
        {1, 3, 2},
        {1, 4, 2},
        {3, 2, 5},
        {3, 1, 1},
        {4, 3, -3}
    };

    int src = 0;
    bellmanFord(edges, numVertices, numEdges, src);

    return 0;
}

3. 弗洛伊德(Floyd)算法

算法思路

Floyd 算法用于求解图中任意两个顶点之间的最短路径,它通过动态规划的思想,逐步考虑每个顶点作为中间点,更新任意两个顶点之间的最短距离。

算法步骤

  1. 初始化:将图的邻接矩阵作为初始的最短距离矩阵。
  2. 动态规划:对于每个顶点k ,考虑以 k为中间点,更新任意两个顶点 i和 j之间的最短距离,即如果 dist[i][k]+dist[k][j]<dist[i][j],则更新dist[i][j]为dist[i][k]+dist[k][j] 。
  3. 重复步骤 2:对所有顶点进行上述操作。
代码实现(C 语言)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 100

// 弗洛伊德算法
void floydWarshall(int graph[MAX_VERTICES][MAX_VERTICES], int numVertices) {
    int dist[MAX_VERTICES][MAX_VERTICES];

    // 初始化
    for (int i = 0; i < numVertices; i++) {
        for (int j = 0; j < numVertices; j++) {
            dist[i][j] = graph[i][j];
        }
    }

    // 动态规划
    for (int k = 0; k < numVertices; k++) {
        for (int i = 0; i < numVertices; i++) {
            for (int j = 0; j < numVertices; j++) {
                if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX && dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }

    // 输出结果
    printf("任意两个顶点之间的最短距离:\n");
    for (int i = 0; i < numVertices; i++) {
        for (int j = 0; j < numVertices; j++) {
            if (dist[i][j] == INT_MAX) {
                printf("INF ");
            } else {
                printf("%d ", dist[i][j]);
            }
        }
        printf("\n");
    }
}

int main() {
    int numVertices = 4;
    int graph[MAX_VERTICES][MAX_VERTICES] = {
        {0, 5, INT_MAX, 10},
        {INT_MAX, 0, 3, INT_MAX},
        {INT_MAX, INT_MAX, 0, 1},
        {INT_MAX, INT_MAX, INT_MAX, 0}
    };

    floydWarshall(graph, numVertices);

    return 0;
}

 

总结

  • Dijkstra 算法:适用于边权为非负的单源最短路径问题,效率较高。
  • Bellman - Ford 算法:可以处理带有负权边的单源最短路径问题,但效率较低。
  • Floyd 算法:适用于求解任意两个顶点之间的最短路径问题,代码简单,但时间复杂度较高。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值