1. 什么是最小生成树
给定一个无向图,如果它的某个子图中任意两个顶点都相互连通并且是一颗树,那么这棵树称为生成树 (Spanning Tree)。如果边上有权值,那么使得边权和最小的生成树叫做最小生成树(MST, Minimum Spanning Tree).
一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 --- 百度百科
求解最小生成树的算法有 Kruskal 算法和 Prim 算法。 很显然生成树是否存在和图是否连通是等价的,因此我们假定图是联通的。
找一个典型问题:
我们假设有这样一个图:把顶点看作村庄,边看作要修的道路。为了在所有村庄见通行,就最少需要修建 V - 1 条道路时的情形就对应了一颗生成树。修建道路需要投入建设费用,那么求解使得道路建设费用最小的生成树就是最小生成树问题。
2. Prim算法(适用稠密图)
Prim 算法 和 Dijkstra 算法十分相似,都是从某点出发,不断添加边的算法。
首先,假设有一棵只包含一个顶点 v 的树 T。然后贪心地选取 T 和其它顶点之间相连的最小权值的边,并把它加到 T 中。不断进行这个操作,就可以得到一棵生成树。接下来我们证明通过这个方法得到的生成树是最小生成树。
证明:
令 V 表示顶点的集合, 假设现在求得的生成树的顶点集合我 X ,并且 T 是 V 上最小生成树的子图。下面我们证明存在一棵最小生成树使得 T 是它的一个子图并且它包含了连接 X 和 V X 之间的边中最小的边。 记这条边为 e ,如果 e 在最小生成树上, 问题就得证了。所以我们假设不在最小生成树上,因为这是棵树, 所以在添加了 e 后就产生了圈。
圈上的边中,必然存在一条和 e 不同的边 f 连接着 X 和 V X,从 e 的定义上看就知道 e ≤ f。因此我们把 f 从树中删掉,然后加上 e 得到一棵新的生成树,并且总权值 ≤ 原来的生成树。像这样不断地加入新边,直到 X = V,那么 T 就是 V 上的最小生成树。
如何查找最小权值的边?
把 X 和 顶点 V 连接的边的最小权值记为 mincost[ v ],在向 X 里添加顶点 u 时,只需要查看和 u 相连的边就可以了。对于每条边,更新 mincost[ v ] = min(mincost[ v ], cost[ u ][ v ]) 即可。如果每次遍历未包含在 X 中的点的 mincost[ v ],需要 0(| V |2)的时间,不过和 Dijkstra 算法一样,我们可以用堆来维护 mincost ,时间复杂度就是 0(| E | log | V |)。
int cost[MAX_V][MAX_V]; // 边的权值,不存在的情况下为INF int mincost[MAX_V]; bool used[MAX_V]; int prim() { for (int i = 0; i < V; i++) { mincost[i] = INF; used[i] = false; } mincost[0] = 0; int res = 0; while(true) { int v = -1; // 从不属于 X 的顶点中选取从 X 到其权值最小的顶点 for (int u = 0; u < V; u++) if (!used[u] && (V == -1 || mincost[u] < mincost[v])) v = u; if(v == -1) break; used[v] = true; // 把顶点 v 加到 X 中 res += mincost[v]; // 把边的长度加到结果里 for (int u = 0; u < V; u++) mincost[u] = min(mincost[u], cost[v][u]); } return res; }
3. Kruskal 算法(适用稀疏图)
Kruskal 算法按照边的权值的顺序从小到大查看一遍,如果不产生圈,就把当前这条边加到生成树中。思想和 Prim 类似。
如何判断是否产生了圈?
假设现在要把连接顶点 u 和顶点 v 的边 e 加入生成树中。如果加入之前 u 和 v 不在同一个连通分量中(这里同时运用了原图和当前生成树),那么加入 e 也不会产生圈。反之, 如果 u 和 v 在同一个连通分量里,那么一定会产生圈。可以用并查集高效地判断是否属于同一个连通分量。
Kruskal 算法在边的排序上最费时间,算法复杂度是0(| E | log | V |)。
struct edge { int u; int v; int cost; }; edge es[MAX_E]; int V, E; bool comp(const edge& e1, const edge& e2) { return e1.cost < e2.cost ; } int kruskal() { sort(es, es + E, comp); init_union_find(V); // 并查集初始化 int res = 0; for (int i = 0; i < E; i++) { edge e = es[i]; if (!same(e.u , e.v)) { unite(e.u , e.v); res += e.cost ; } } return res; }
完整Code:
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; const int MAX_E = 1000; const int MAX_V = 100; const int MAX_N = 1000; const int MAX_R = 1000; struct edge{ int u; int v; int cost; }; edge es[MAX_E]; int V, E; int par[MAX_N]; int rank[MAX_N]; bool comp(const edge& e1, const edge& e2) { return e1.cost < e2.cost ; } void init(int n) { for (int i = 0; i < n; i++) { par[i] = i; rank[i] = 0; } } int find(int x) { if (par[x] == x) return x; else return par[x] = find(par[x]); } void unite(int x, int y) { x = find(x); y = find(y); if(x == y) return; if(rank[x] < rank[y]) par[x] = y; else { par[y] = x; if (rank[x] == rank[y]) rank[x]++; } } bool same(int a, int b) { if (find(a) == find(b)) return true; else return false; } int kruskal() { sort(es, es + E, comp); int res = 0; init(V + 1); for (int i = 0; i < E; i++) { edge e = es[i]; if (!same(e.u, e.v)) { unite(e.u, e.v); res += e.cost; } } return res; } int main() { cin >> V >> E; for (int i = 0; i < E; i++) cin >> es[i].u >> es[i].v >> es[i].cost ; cout << kruskal() << endl; }
应用问题:
设想一个这样的无向图:在征募某个人 a 时,如果使用了 a 和 b 之间的关系,那么就连一条 a 到 b 的边。假设这个图中存在圈,那么无论以什么顺序征募这个圈上的所有人,都会产生矛盾。因此可以知道这个图是一片森林。反之,如果给了一片森林,那么就可以使用对应的关系确定征募的顺序。
因此,把人看做顶点,关系看作边,这个问题就可以转化为求解无向图中的最大权森林问题。最大权森林问题可以通过把所有边权值取反之后用最小生成树的算法求解。
int N, M, R; int x[MAX_R], y[MAX_R], d[MAX_R]; void solve() { V = N + M; E = R; for (int i = 0; i < R; i++) es[i] = (edge){x[i], N + y[i], -d[i]}; printf("%d ", 10000 * (N + M) + kruskal()); }