
最小生成树Prim算法
与Dijkstra算法类似,任意挑一个顶点,添加最短边,直至所有顶点都在树中,此时就得到一颗最小生成树了。
证明:
令V为顶点集合,已求得顶点集合为X,V上的最小生成树为T。
假设连接X和V\X的最短边为e,现在需要证明T包含e。
如果T不包含e的话,将e加入T形成一个圈(e是圈的一条边),那么e的两个端点X和V\X之间必定还有一条边f,且f权重比e大,假如删掉f,此时的树成了一颗比最小生成树还小的树,形成矛盾。
Prim算法伪码
MST-PRIM(G, w, r)
1 for each u ∈ G.V
2 u.key ← ∞
3 u.π ← NIL
4 r.key ← 0
5 Q ← G.V
6 while Q ≠ Ø
7 u ← EXTRACT-MIN(Q)
8 for each v ∈ G.Adj[u]
9 if v ∈ Q and w(u, v) < v.key
10 v.π ← u
11 v.key ← w(u, v)
Prim算法复杂度分析
使用最小堆实现Q的话,
-
第5行构造最小堆耗时O(V)。
-
然后EXTRACT-MIN(Q)=O(log V),一共调用V次,所以第6和7行复杂度为O(VlogV)。
-
第8行一共调用2E次(无环图),每次的内层循环复杂度多少呢?第11行修改了最小堆中的一个元素,这个元素需要调整自己的位置,这在最小堆中的代价是O(log V)。
最终复杂度为O(V+VlogV+ElogV)=O(ElogV)。
使用FIBONACCI实现Q的话,
-
相同
-
相同
-
依然调用2E次,但每次复杂度O(1)
最终复杂度为O(V+VlogV+E)=O(E+VlogV)。
为什么我们在上面的最终计算中都忽略了V?因为E的大小是V的平方。
Prim算法的实现
记链接X和V的边的最小权值为mincost[v],添加新顶点u的时候,只需更新u相连的顶点的最小权值即可。
mincost[v] = min(mincost[v] , weight(u, v))。
如果直接遍历mincost找出最小值的话,复杂度为O(|V|2),使用最小堆的话复杂度降低为O(|E|log|V|)。
#include <iostream>
#include <queue>
#include <functional>
using namespace std;
#define MAX_V 1024
int mincost[MAX_V]; // 从集合X出发的边到每个顶点的最小权值
bool used[MAX_V]; // 顶点i是否包含在集合X中
int V; // 顶点数
// first 最短路径,second顶点编号
typedef pair<int, int> P;
struct edge
{
int to, cost;
edge(int to = 0, int cost = 0) : cost(cost), to(to) {}
};
// 边
vector<edge> G[MAX_V]; // G[i] 顶点i到G[i].to的权值为G[i].cost
int prim()
{
int res = 0;
memset(mincost, 0x3f, V * sizeof(int));
memset(used, 0, V * sizeof(bool));
mincost[0] = 0;
priority_queue<P, vector<P>, greater<P> > que;
que.push(P(0, 0));
while (!que.empty())
{
P p = que.top(); que.pop();
int v = p.second;
if (used[v] || p.first > mincost[v]) continue;
used[v] = true;
res += mincost[v];
for (int i = 0; i < G[v].size(); ++i)
{
edge e = G[v][i];
if (mincost[e.to] > e.cost)
{
mincost[e.to] = e.cost;
que.push(P(mincost[e.to], e.to));
}
}
}
return res;
}
///////////////////////////SubMain//////////////////////////////////
int main(int argc, char *argv[])
{
// 测试数据
V = 7;
G[0].push_back(edge(1, 10));
G[1].push_back(edge(0, 10));
G[0].push_back(edge(2, 2));
G[2].push_back(edge(0, 2));
G[1].push_back(edge(3, 5));
G[3].push_back(edge(1, 5));
G[2].push_back(edge(3, 7));
G[3].push_back(edge(2, 7));
G[2].push_back(edge(4, 1));
G[4].push_back(edge(2, 1));
G[2].push_back(edge(5, 3));
G[5].push_back(edge(2, 3));
G[3].push_back(edge(5, 1));
G[5].push_back(edge(3, 1));
G[3].push_back(edge(6, 8));
G[6].push_back(edge(3, 8));
G[5].push_back(edge(6, 5));
G[6].push_back(edge(5, 5));
cout << prim() << endl;
system("pause");
return 0;
}
///////////////////////////End Sub//////////////////////////////////
最小生成树Kruskal算法
按照边的权值从小到大尝试加入最小树,如果不产生圈就加进去,否则就扔掉。
关于判断是否产生圈,可以利用并查集来高效地实现。连通的顶点放入一个并查集(连通分量)里,如果一条边e的两个顶点u, v不在同一个连通分量中,那么将e加进来也不会产生圈。
最小生成树Kruskal算法的实现
#pragma warning(disable : 4996)
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX_E 1024
// 并查集相关数据与算法
#define MAX_N MAX_E + 16
int parent[MAX_N];
int height[MAX_N];
void init(const int& n)
{
for (int i = 0; i < n; ++i)
{
parent[i] = i;
height[i] = 0;
}
}
int find(const int& x)
{
if (parent[x] == x)
{
return x;
}
else
{
return parent[x] = find(parent[x]);
}
}
void unite(int x, int y)
{
x = find(x);
y = find(y);
if (x == y)
{
return;
}
if (height[x] < height[y])
{
parent[x] = y;
}
else
{
parent[y] = x;
if (height[x] == height[y])
{
++height[x];
}
}
}
bool same(const int& x, const int& y)
{
return find(x) == find(y);
}
// End Of 并查集
struct edge
{
int u, v, cost;
edge(int u = 0, int v = 0, int cost = 0) : u(u), v(v), cost(cost) {}
bool operator < (const edge & e2) const
{
return cost < e2.cost;
}
};
edge es[MAX_E];
int V, E;
int kruskal()
{
sort(es, es + E); // 按照权值从小到大排序
init(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;
}
///////////////////////////SubMain//////////////////////////////////
int main(int argc, char *argv[])
{
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
// 测试数据就不手工填充了,好累( ̄▽ ̄")
cin >> V >> E;
for (int i = 0; i < E; ++i)
{
cin >> es[i].u >> es[i].v >> es[i].cost;
}
cout << kruskal() << endl;
fclose(stdin);
fclose(stdout);
system("out.txt");
return 0;
}
///////////////////////////End Sub//////////////////////////////////
码农场
假设连接X和VX的最短边为e,现在需要证明T包含e
这里的VX是什么意思
V去掉X