Prim和Kruskal算法应用----城市水管连接

Prim和Kruskal算法应用----城市水管连接

问题描述:

Description:

现在有n个城镇,编号为1, 2, 3, 4…n。他们之间有m条互通的道路,每条道路有相应的长度,现在基于这些道路,选择其中的一部分,在其上铺设水管,水管长度等于道路长度。要求使得任意两个城市之间都能通水,即任何城市之间可以通过水管连接。求使得所有城市连接的水管最短长度。

Input:

第一行两个数,分别表示n和m (1<=n<=10001,1<=m<=10000*10000/2)。

后面为m行,其中每行3个数,分别表示连接的第一个城市,第二个城市,和道路长度,道路长度为大于0的整数。

Output

输出为1行,即水管最短长度。提示,如果不存在一种方案使得所有城市之间都能两两相通,那么输出的最短长度为-1。

Sample Input 1

4 5
1 2 1
1 3 1
1 4 2
2 4 3
3 4 3

Sample Output 1

4

Prim算法

该算法是采用贪婪策略进行设计的一种算法,有点类似于求最短路径的Dijkstra算法

1. Prim算法的思想方法

令G=(V,E,W),为简单起见,令顶点集为V={0,1,…,n-1}。用二维数组c来存放边集E中边的权,若与顶点i,相关联的边为eij,则eij的权为c[i][j]。假定T是最小花费生成树的边集。该算法维护两个集合S和N。开始时,令T=φ,S={0},N=V-S。然后,进行贪婪选择,选取i∈S,j∈N,并且c[i][j]最小,使S=S∪{j},N=N-{j},T=T∪{eij}。重复上述步骤,直到N为空,或找到n-1条边为止。此时,T中的边集就是我们要求取的G中的最小花费生成树。由此,Prim算法的步骤可描述如下:

(1)T=φ,S={0},N=V-S。

(2)如果N为空,则算法结束,否则转步骤(3)

(3)寻找使i∈S,j∈N,并且c[i][j]最小的i和j。

(4)S=S∪{j},N=N-{j},T=T∪{eij},转步骤(2)

具体问题具体分析,因为我上面的问题的边数最大可达10000*10000/2,若全用数组存储,会占据过多内存,所以我们就用邻接表来存储边的权值,而且我们只需要输出代价即可,不需要输出整个树,所以这里的T我们就不需要了,将上述思想稍加修改一下即可得到:

若与顶点i,相关联的边为eij,则eij的权为c[i][j]。该算法维护两个集合S和N。开始时,令sum=0,S={0},N=V-S。然后,进行贪婪选择,选取i∈S,j∈N,并且c[i][j]最小,使S=S∪{j},N=N-{j},sum累加j的代价。重复上述步骤,直到N为空,或找到n-1条边为止。此时,sum中的值就是我们要求取的最终代价。由此,Prim算法的步骤可描述如下:

(1)sum=0,S={0},N=V-S。

(2)如果N为空,则算法结束,否则转步骤(3)

(3)寻找使i∈S,j∈N,并且c[i][j]最小的i和j。

(4)S=S∪{j},N=N-{j},sum累加j的代价,转步骤(2)

2 .prim算法的代码

#include<iostream>
using namespace std;
typedef struct ArcNode {//边
	int adjvex;//该边指向的顶点的下标,也就是邻接的顶点的编号
	int info;//该边的权值
	struct ArcNode *nextarc;//该边的下一个边
	ArcNode() :adjvex(0),info(0),nextarc(nullptr) {}
}ArcNode;
typedef struct VNode {//顶点
	ArcNode *firstarc;
	//一般情况下每个顶点可能还有其他信息,这里比较简单就没有添加其他信息
	//Type data;
	VNode():firstarc(nullptr){}//该顶点的第一个弧(边)
}VNode;
typedef struct Graph
{
	VNode* vertexs;//这是一个邻接表的首指针
	int vexnum;//邻接表的顶点个数
	int arcnum;//邻接表的边数
	Graph() :vertexs(nullptr), vexnum(0), arcnum(0){}
}ALGraph;
ALGraph G;
void TailCreat()//尾插法创建链表 
{
	int i, k, j, info;
	ArcNode *p=nullptr, *s=nullptr, *q=nullptr;
	int x, y;
	for (k = 0; k < G.arcnum; k++)
	{
		cin >> x >> y >> info;//要创建无向图,需要创建两次边
		s = new ArcNode;
		s->adjvex = y;
		s->info = info;
		p = G.vertexs[x].firstarc;
		if (p == nullptr)//p为空表示这个顶点后面没有任何的节点 
		{
			G.vertexs[x].firstarc = s;
		}
		else //若不空则遍历到最后一个,然后连接 
		{
			while (p)
			{
				q = p;
				p = p->nextarc;//p最后是nullptr,q是最后一个节点 
			}
			q->nextarc = s;//将s接到最后一个节点上 
		}
        //同上,再建立一个边
		s = new ArcNode;
		s->adjvex = x;
		s->info = info;
		p = G.vertexs[y].firstarc;
		if (p == nullptr)//p为空表示这个顶点后面没有任何的节点 
		{
			G.vertexs[y].firstarc = s;
		}
		else //若不空则遍历到最后一个,然后连接 
		{
			while (p)
			{
				q = p;
				p = p->nextarc;//p最后是NULL,q是最后一个节点 
			}
			q->nextarc = s;//将s接到最后一个节点上 
		}
	}
}
int prim_(int n) {
	int i, j, u,sum=0;
	//数组的第一个元素不要,下标从1开始
	bool *s = new bool[n+1];
	int *neig = new int[n+1];//neig[j]中的元素是集合S中的节点到第j个节点距离最小的那个值的下标
	int min, *w = new int[n+1];//w[j]中的元素是集合S中的节点到第j个节点的距离的最小值
	s[1] = true;//把第一个顶点并入集合S
	//初始化集合N中个顶点的初始状态
	ArcNode *e = nullptr;
	e = G.vertexs[1].firstarc;
	//从2开始各个节点初始化
	for (i = 2; i <= n; i++) {
		w[i] = INT_MAX;
		neig[i] = 0;
		s[i] = false;
	}
	while (e != nullptr) {//遍历顶点1的所有邻接边
		w[e->adjvex] = e->info;
		e = e->nextarc;
	}
	
	for (i = 1; i < n; i++) {//i用来控制循环的次数,共n-1条边
		u = 0;
		min = INT_MAX;
		for(j=2;j<=n;j++)//从2号顶点开始寻找尚未加入S,且从S中各个顶点到这个顶点的边的权值最小的那个边
			if (!s[j] && w[j] < min) {
				u = j; min = w[j];
			}
		if (u == 0)return -1;//此图为非连通图,因为S无法到底N
		sum += w[u]; s[u] = true;//符合条件的边累加到sum,将u并入S
		e = G.vertexs[u].firstarc;
		while(e!=nullptr) {//更新w和neig数组
			if (!s[e->adjvex] && e->info < w[e->adjvex]) {
				//边所指向的顶点尚未加入N且这条边的权值小于之前指向这个顶点的最小值
				w[e->adjvex] = e->info;
				neig[e->adjvex] = u;
			}
			e = e->nextarc;
		}
	}
	delete s; delete w; delete neig;
	return sum;
}
int main() {
	int n, m;//输入顶点数和弧的个数
	cin >> n >> m;
	VNode vtemp;
	ArcNode* etemp;
	//对G的顶点进行初始化
	G.vertexs = new VNode[n+1];//第一个顶点不要
	G.vexnum = n;
	G.arcnum = m;
	//创建G的边
	TailCreat();
    //prim
	cout << prim_(n);
	return 0;
}

3 .复杂度分析

时间复杂度:Prim算法最外层n-1次循环寻找n-1个边中的代价,内部也是n-1次循环找N与S中最接近的边,时间复杂度共是O(n^2)

空间复杂度:从上述算法可以看出用于工作单元的空间是O(n)

Kruskal算法

此算法也是使用贪婪法策略设计的典型算法,但是和Prim算法完全不同

1 .Kruskal算法的思想方法

Kruskal算法俗称避环法。其思想方法如下:开始时,把图的所有顶点都作为孤立顶点,每个顶点都构成一棵只有根节点的树,由这些树构造一个森林T。然后,把所有的边按权的非降顺序排序,构成边集的一个非降序列。从边集中取出权最小的一条边,如果把这条边加入森林T中,不会使T构成回路,就把它加入森林中(或者把森林中某两棵树连接成一棵树);否则,就放弃它。在这种情况下,都把它从边集中删去。重复这个过程,直到把n-1条边都放到森林以后,结束这个过程。这时,该森林中所有的树就被连接成一棵树T,它就是所要求的图的最小花费生成树。

在把边e加入T中时,如果与边e相关联的顶点u和v分别在两棵树上,随着边e的加入,将使这两棵树合并成一棵树;如果与边e相关联的顶点u和v都在同一棵树上,则新加入的边e,将把这两个结点连接起来,使原来的树构成回路。为了判断把边e加入T中是否会构成回路,可以使用第3章所叙述的find(u)、find(v)操作及union(u,v)操作。前两个操作寻找u和v所在树的根结点,如果find(u)、find(v)操作表明u和v的根结点不想同,则继续执行的union(u,v)操作,将把边e加入T中,并使u和v所在的两棵树合并成一棵树;如果find(u)、find(v)操作表明u和v的根结点相同,则u和v同在一棵树上,这时就不再执行union(u,v)操作,并丢弃边e。

于是,对无向连通赋权图G=(V,E,W),求该图的最小花费生成树的Kruskal算法的步骤可叙述如下:

(1)按权的非降顺序排序E中的边
(2)令最小花费生成树的边集为T,T初始化为T=φ
(3)把每个顶点都初始化为树的根结点
(4)令e=(u,v)是E中权最小的边,E=E-{e}
(5)如果find(u)≠find(v),则执行union(u,v)操作,T=T∪{e}
(6)如果|T|<n-1,转步骤(4),否则,算法结束

同Prim算法,具体问题具体分析,同样是将边集T换成sum,这里就不详细说明了,读者应该学会自己总结归纳,凡事跟你说的明明白白,你就无法成长~,懂我意思吧。

堆和并查集的操作见郑宗汉老师的书《算法设计与分析》第3章3.2和3.4,因为c++ 11中union变成了关键字,所以我就把union函数改名为join函数。

2 .Kruskal算法的代码

#include<iostream>
using namespace std;
typedef struct edge{
	int key;
	int u;
	int v;
	edge():key(0),u(0),v(0){}
}EDGE;
typedef struct node {

	struct node *p;
	int rank;
	int u;
	node():p(nullptr),rank(0),u(0){}
}NODE;

NODE* find(NODE* xp) {
	NODE *wp, *yp = xp, *zp = xp;
	while (yp->p != nullptr) yp = yp->p;

	while (zp->p != nullptr) {//路径压缩
		wp = zp->p;
		zp->p = yp;
		zp = wp;
	}
	return yp;
}
NODE* join(NODE *xp, NODE *yp) {
	NODE *up, *vp;
	up = find(xp);
	vp = find(yp);
	if (up->rank <= vp->rank) {
		up->p = vp;
		if (up->rank == vp->rank)
			vp->rank++;
		up = vp;
	}
	else
		vp->p = up;
	return up;
}
void sift_down(EDGE E[], int m, int i) {//若第i个节点大于其儿子节点,进行循环下移
	bool done = false;
	while (!done && ((i = 2 * i) <= m)) {
		//两个儿子节点进行比较,父亲节点将与较小的儿子节点比较
		if ((i + 1 <= m)&& (E[i + 1].key < E[i].key))i++;

		//E[i]是儿子节点,E[i/2]是父亲节点
		if (E[i / 2].key > E[i].key) swap(E[i / 2], E[i]);
		else done = true;//父亲节点不大于儿子节点
	}
}
void sift_up(EDGE E[],int i) {
	bool done = false;
	while (!done&&i != 1) {
		//子节点小于父节点则需要调整
		if (E[i].key < E[i / 2].key)
			swap(E[i], E[i / 2]);
		else done = true;
		i = i / 2;
	}
}
void make_heap(EDGE E[], int m) {
	int i;
	//E[m] = E[0];//若数组从0开始索引则需要
	for (i = m / 2; i >= 1; i--)
		sift_down(E,m,i);
}
void delete_i(EDGE E[], int &m, int i) {//删除第i个节点
	EDGE e;//用来存储被删除的节点
	e = E[i];
	if (i <= m) {
		E[i] = E[m];
		m--;
		//若E[m]比被删除的节点小,则有可能需要上移,否则可能下移 
		if (E[i].key < e.key)
			sift_up(E, i);
		else
			sift_down(E, m, i);
	}
}
EDGE delete_min(EDGE E[], int &m) {
	EDGE e;
	e = E[1];
	delete_i(E, m, 1);
	return e;
}
int kruskal(NODE V[], EDGE E[], int n, int m) {
	//V是顶点,E是边,n是顶点数,m是边数
	int i=0, sum=0;
	EDGE e;
	NODE *u, *v;
	make_heap(E, m);//小堆
	//V[i]的rank和p已经初始化为0和nullptr

	while ((i < n - 1) && (m > 0)) {
		e = delete_min(E, m);//从最小堆中取下权最小的边
		u = find(&V[e.u]);
		v = find(&V[e.v]);
		if (u != v) {
			join(u, v);
			sum += e.key;//符合要求的边累加到sum中
			i++;
		}
	}
	if (i == n - 1)return sum;
	return -1;//这是一个非连通图,所以无最小生成树
}
int main() {
	int n, m;//先输入顶点和边的个数
	cin >> n >> m;
	//根据顶点和边的个数创建顶点和边
	auto V = new NODE[n + 1];
	auto E = new EDGE[m + 1];//根据堆的性质,索引从1开始

	for (int i = 1; i <= m; i++) {
		cin >> E[i].u >> E[i].v >> E[i].key;//输入从u到v的权值
	}

	cout<<kruskal(V, E, n, m);
	delete V, delete E;
	return 0;
}

3 .复杂度分析

时间复杂度:find操作最多2m次,因此总花费时间最多为O(mlog*n)。构建小堆花费O(mlogm)。若所处理的是一个完全图,那么将有m=n(n-1)/2,此时,所花费的时间为O(n^2logn),若所处理的是一个平面图,那么将有m=O(n),这时,所花费的时间为O(nlogn)。

空间复杂度:因为没有边集,所以就是O(1)

总结

时间复杂度空间复杂度
Prim算法O(n^2)O(n)
Kruskal算法最坏O(n^2logn),最好O(nlogn)O(1)

总的来说这两个算法各有千秋,应该根据不同的应用场景去采纳不同的算法。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值