Administrator
Administrator
发布于 2024-09-06 / 2 阅读
0
0

第6节:高级图算法

我们将继续深入学习图论的高级图算法。这些算法解决了一些经典的图论问题,例如最短路径、最小生成树、图的连通性等。这些问题在实际应用中有着广泛的使用,如网络优化、交通路线规划、数据流分析等。

第6节:高级图算法

6.1 最短路径算法

最短路径问题是指在图中找到从一个顶点到另一个顶点的最短路径。根据图的性质,常见的最短路径算法包括:

  1. Dijkstra算法(适用于无负权边的图)
  2. Bellman-Ford算法(适用于有负权边的图)
  3. Floyd-Warshall算法(用于计算所有顶点对之间的最短路径)
1. Dijkstra算法

Dijkstra算法用于计算从单个源点到其他所有顶点的最短路径,前提是边的权值都是非负的。该算法使用贪心策略,通过选择未处理的距离最小的顶点,逐步构造最短路径树。

Dijkstra算法的实现(使用邻接矩阵表示图):

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

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

// 找到未访问顶点中距离最小的顶点
int minDistance(int dist[], int visited[]) {
    int min = INT_MAX, minIndex;

    for (int v = 0; v < V; v++) {
        if (!visited[v] && dist[v] <= min) {
            min = dist[v], minIndex = v;
        }
    }
    return minIndex;
}

// 打印源点到其他顶点的最短路径
void printSolution(int dist[]) {
    printf("Vertex\tDistance from Source\n");
    for (int i = 0; i < V; i++) {
        printf("%d\t%d\n", i, dist[i]);
    }
}

// Dijkstra算法的实现
void dijkstra(int graph[V][V], int src) {
    int dist[V]; // dist[i] 保存源点到i的最短路径距离
    int visited[V]; // visited[i]表示顶点i是否已被处理

    // 初始化:所有顶点的距离为无穷大,所有顶点未被访问
    for (int i = 0; i < V; i++) {
        dist[i] = INT_MAX;
        visited[i] = 0;
    }

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

    for (int count = 0; count < V - 1; count++) {
        int u = minDistance(dist, visited); // 选取未访问顶点中距离最小的顶点
        visited[u] = 1;

        for (int v = 0; v < V; v++) {
            if (!visited[v] && graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }

    printSolution(dist);
}

int main() {
    // 图的邻接矩阵表示
    int graph[V][V] = {
        {0, 10, 0, 30, 100},
        {10, 0, 50, 0, 0},
        {0, 50, 0, 20, 10},
        {30, 0, 20, 0, 60},
        {100, 0, 10, 60, 0}
    };

    dijkstra(graph, 0); // 从顶点0开始计算最短路径

    return 0;
}

运行结果:

Vertex  Distance from Source
0       0
1       10
2       50
3       30
4       60
2. Bellman-Ford算法

Bellman-Ford算法用于有负权边的图,它能够检测负权环。如果图中存在负权环,则说明不存在有效的最短路径,因为路径的权重可以无限小。

Bellman-Ford算法的实现(使用邻接矩阵表示图):

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

#define V 5 // 顶点数量
#define E 8 // 边数量

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

// 打印最短路径结果
void printArr(int dist[], int n) {
    printf("Vertex\tDistance from Source\n");
    for (int i = 0; i < n; i++) {
        printf("%d\t%d\n", i, dist[i]);
    }
}

// Bellman-Ford算法
void bellmanFord(struct Edge edges[], int src) {
    int dist[V];

    // 初始化所有顶点的距离为无穷大,源点距离为0
    for (int i = 0; i < V; i++) {
        dist[i] = INT_MAX;
    }
    dist[src] = 0;

    // 对所有边进行V-1次松弛操作
    for (int i = 1; i <= V - 1; i++) {
        for (int j = 0; j < E; 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 i = 0; i < E; i++) {
        int u = edges[i].src;
        int v = edges[i].dest;
        int weight = edges[i].weight;
        if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {
            printf("Graph contains negative weight cycle\n");
            return;
        }
    }

    printArr(dist, V);
}

int main() {
    struct Edge edges[E] = {
        {0, 1, -1}, {0, 2, 4}, {1, 2, 3},
        {1, 3, 2}, {1, 4, 2}, {3, 2, 5},
        {3, 1, 1}, {4, 3, -3}
    };

    bellmanFord(edges, 0); // 从顶点0开始计算最短路径

    return 0;
}

运行结果:

Vertex  Distance from Source
0       0
1       -1
2       2
3       -2
4       1
3. Floyd-Warshall算法

Floyd-Warshall算法用于计算所有顶点对之间的最短路径,适用于有向图。该算法通过动态规划的思想,在三重循环中逐步更新最短路径矩阵。

Floyd-Warshall算法的实现:

#include <stdio.h>

#define V 4
#define INF 99999 // 用于表示无穷大的距离

// 打印矩阵
void printSolution(int dist[V][V]) {
    printf("Shortest distances between every pair of vertices:\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");
    }
}

// Floyd-Warshall算法
void floydWarshall(int graph[V][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++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }

    printSolution(dist);
}

int main() {
    int graph[V][V] = {
        {0, 5, INF, 10},
        {INF, 0, 3, INF},
        {INF, INF, 0, 1},
        {INF, INF, INF, 0}
    };

    floydWarshall(graph);
    return 0;
}

运行结果:

Shortest distances between every pair of vertices:
      0      5      8      9
    INF      0      3      4
    INF    INF      0      1
    INF    INF    INF      0

6.2 最小生成树算法

最小生成树(MST)是图中的一棵树,它包含图中的所有顶点且边的总权值最小。常见的最小生成树算法有:

  1. Kruskal算法:使用贪心策略,基于边的权重,逐渐加入最小权重的边,直到构造出一棵生成树。
  2. Prim算法:基于顶点逐步扩展生成树,每次选择未加入树的顶点中与树最接近的顶点。
1. Kruskal算法

Kruskal算法通过逐步选择最小权重的边来构建最小生成树,通常使用**并查集(Disjoint Set Union, DSU)**来检测环。

Kruskal算法的实现

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

#define V 4 // 顶点数量
#define E 5 // 边数量

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

struct Graph {
    struct Edge edges[E];
};

// 并查集:查找根节点
int find(int parent[], int i) {
    if (parent[i] == i) {
        return i;
    }
    return find(parent, parent[i]);
}

// 并查集:合并两个集合
void unionSets(int parent[], int rank[], int x, int y) {
    int xroot = find(parent, x);
    int yroot = find(parent, y);

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

// 比较函数用于根据边的权重排序
int compare(const void* a, const void* b) {
    struct Edge* edgeA = (struct Edge*)a;
    struct Edge* edgeB = (struct Edge*)b;
    return edgeA->weight - edgeB->weight;
}

// Kruskal算法
void kruskal(struct Graph* graph) {
    struct Edge result[V]; // 最小生成树的边
    int parent[V];
    int rank[V];

    // 初始化并查集
    for (int v = 0; v < V; v++) {
        parent[v] = v;
        rank[v] = 0;
    }

    // 按权重对边进行排序
    qsort(graph->edges, E, sizeof(graph->edges[0]), compare);

    int e = 0; // 已加入的边数
    int i = 0; // 当前考虑的边

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

        int x = find(parent, nextEdge.src);
        int y = find(parent, nextEdge.dest);

        if (x != y) { // 如果不构成环
            result[e++] = nextEdge;
            unionSets(parent, rank, 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);
    }
}

int main() {
    struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));

    graph->edges[0] = (struct Edge){0, 1, 10};
    graph->edges[1] = (struct Edge){0, 2, 6};
    graph->edges[2] = (struct Edge){0, 3, 5};
    graph->edges[3] = (struct Edge){1, 3, 15};
    graph->edges[4] = (struct Edge){2, 3, 4};

    kruskal(graph);

    return 0;
}

运行结果:

Following are the edges in the constructed MST:
2 -- 3 == 4
0 -- 3 == 5
0 -- 1 == 10

练习题

  1. Prim算法:实现Prim算法,使用优先队列来选择权重最小的边。
  2. Dijkstra vs Bellman-Ford:编写程序对比Dijkstra和Bellman-Ford算法在有负权边图上的表现。
  3. 图的环检测:使用DFS或并查集实现无向图和有向图的环检测。
  4. 拓扑排序:在有向无环图(DAG)中,使用DFS实现拓扑排序。
  5. 旅行商问题:实现旅行商问题的近似解法。

完成这些练习后,你将掌握图的高级算法和技术。


评论