摘自计蒜客:
http://www.jisuanke.com/course/35/7551
讨论图论的一个经典问题——最小生成树。什么是最小生成树呢?我们先来看这样一个问题:
有 n 座城市,现要给城市间铺设高铁,使得任意两座城市之间都可以高铁到达。现已知任意两座城市之间铺设高铁的费用,求问如何铺设可以使得总费用最小。
这就是一个经典的最小生成树问题。首先可以把该场景看成是一个带权图,城市就是图中的顶点,边的权值就是城市间铺设高铁的费用。另外我们可以知道n个顶点,只需要n-1条边就能让任意两点连通。
那么问题就转化成了:如何从一个带权图中抽出一棵生成树,使得边权值和最小,这棵生成树就叫做最小生成树。常见的求解最小生成树的算法有Prim算法和Kruskal算法。
先来学习Prim算法。首先我们定义带权图 G 的顶点集合为V,接着我们再定义最小生成树的顶点集合为 U,初始集合 U为空。接着执行以下操作:
1.首先我们任选一个顶点 x,加入集合 U,并记录每个顶点到当前最小生成树的最短距离。
2.选择一个距离当前最小生成树最近的、且不属于集合 U 的顶点 v(如果有多个顶点 v,任选其一即可),将顶点 v 加入集合 U,并更新所有与顶点 v 相连的顶点到当前最小生成树的最短距离。
重复第二步操作,直至集合 U 等于集合 V。
最小生成树构造完毕,集合 U 记录了最小生成树的所有边。
讨论图论的一个经典问题——最小生成树。什么是最小生成树呢?我们先来看这样一个问题:
有 n 座城市,现要给城市间铺设高铁,使得任意两座城市之间都可以高铁到达。现已知任意两座城市之间铺设高铁的费用,求问如何铺设可以使得总费用最小。
这就是一个经典的最小生成树问题。首先可以把该场景看成是一个带权图,城市就是图中的顶点,边的权值就是城市间铺设高铁的费用。另外我们可以知道n个顶点,只需要n-1条边就能让任意两点连通。
那么问题就转化成了:如何从一个带权图中抽出一棵生成树,使得边权值和最小,这棵生成树就叫做最小生成树。常见的求解最小生成树的算法有Prim算法和Kruskal算法。
先来学习Prim算法。首先我们定义带权图 G 的顶点集合为V,接着我们再定义最小生成树的顶点集合为 U,初始集合 U为空。接着执行以下操作:
1.首先我们任选一个顶点 x,加入集合 U,并记录每个顶点到当前最小生成树的最短距离。
2.选择一个距离当前最小生成树最近的、且不属于集合 U 的顶点 v(如果有多个顶点 v,任选其一即可),将顶点 v 加入集合 U,并更新所有与顶点 v 相连的顶点到当前最小生成树的最短距离。
重复第二步操作,直至集合 U 等于集合 V。
最小生成树构造完毕,集合 U 记录了最小生成树的所有边。
举例模拟Prim算法如下:
通过模拟,最终可以得到该图对应的最小生成树,如下图所示:
分析算法过程,Prim算法的思想类似贪心策略,每次都会选择一条与当前最小生成树相连且边权值最小的点。Prim算法的时间
复杂度O(V2),V为图G顶点总个数,如果加上堆优化的话,可以把时间复杂度降到O(VlogV + E),其中E为图G的总边数。Prim算法
一般应用于边较为稠密的图,也就是顶点较少、而边较多的图。
算法实现://目标是从起点v出发,求解一个无向图的最小生成树上的所有边权的和。
#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int INF = 0x3f3f3f3f;
struct Edge {
int vertex, weight;//分别表示这条边的另一个端点和边的权重
};
class Graph {
private:
int n;
bool * visited;
vector<Edge> * edges;
public:
//用来标记每个顶点距离生成树上所有点的最短距离。
//Prim算法就是每次找到距离生成树最近的顶点,并将它加入到生成树上。
int * dist;
Graph (int input_n) {
n = input_n;
edges = new vector<Edge>[n];
dist = new int[n];
visited = new bool[n];
memset(visited, false, n * sizeof(bool));
memset(dist, 0x3f, n * sizeof(int));
}
~Graph() {
delete[] dist;
delete[] visited;
delete[] edges;
}
void insert(int x, int y, int weight) {
edges[x].push_back(Edge{y, weight});
edges[y].push_back(Edge{x, weight});
}
int prim(int v) {
//[1]初始化变量
int total_weight = 0;
dist[v] = 0;
//[2]不断找到距离生成树最近的顶点,并将它插入到当前生成树中
for (int i=0; i<n; ++i) {
int min_dist=INF, min_vertex;
//从n个顶点中找到最近的顶点
for(int j=0; j<n; ++j) {
//[3]对于每个枚举到的顶点,如果之前没有加入到生成树,且距离小于当前的最小值,
//就用它来更新最小值。首先用dist数组中对应的结果更新min_dist,然后将顶
//点编号保存到min_vertex中。
if(!visited[j] && dist[j] < min_dist) {
min_dist = dist[j];
min_vertex = j;
}
}
//[4]将min_vertex插入到生成树中。首先将min_dist累加到生成树的边权和total_weight
//中,并将标记顶点是否在生成树上的visited数组对应元素设为1,表示已插入生成树。
total_weight += min_dist;
visited[min_vertex] = 1;
//[5]枚举min_vertex的每条边,枚举变量命名为j,类型为Edge的引用。
for(Edge &j: edges[min_vertex]) {
//对于每条边,如果另一个端点还未插入到生成树中,
//并且当前枚举的边的边权比它到生成树
//的最小距离要小,那么就用这条边的边权更新它到生成树的最小距离吧。
if (!visited[j.vertex] && j.weight < dist[j.vertex]) {
dist[j.vertex] = j.weight;
}
}
}
return total_weight;
}
};
int main() {
int n, m;
cin >> n >> m;
Graph g(n);
for (int i = 0; i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
g.insert(a, b, c);
}
cout << g.prim(0) << endl;
return 0;
}