放牧代码和思想
专注自然语言处理、机器学习算法

最小生成树算法初步

目录

最小生成树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的话,

  1. 第5行构造最小堆耗时O(V)。

  2. 然后EXTRACT-MIN(Q)=O(log V),一共调用V次,所以第6和7行复杂度为O(VlogV)。

  3. 第8行一共调用2E次(无环图),每次的内层循环复杂度多少呢?第11行修改了最小堆中的一个元素,这个元素需要调整自己的位置,这在最小堆中的代价是O(log V)。

最终复杂度为O(V+VlogV+ElogV)=O(ElogV)。

使用FIBONACCI实现Q的话,

  1. 相同

  2. 相同

  3. 依然调用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//////////////////////////////////

知识共享许可协议 知识共享署名-非商业性使用-相同方式共享码农场 » 最小生成树算法初步

分享到:更多 ()

评论 2

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #1

    假设连接X和VX的最短边为e,现在需要证明T包含e
    这里的VX是什么意思

    奥特曼3年前 (2015-02-18)回复

我的开源项目

HanLP自然语言处理包基于DoubleArrayTrie的Aho Corasick自动机