一篇菜鸡的笔记——Prim算法详解

真叫人头大,又来了个算法,Dijkstra还没看懂,半路又杀出个Prim( ╯□╰ )。那么Prim到底是个什么玩意儿呢(・∀・(・∀・(・∀・*)

先来个栗子o(●’◡’●)┛

镇长修路

假设你是一个镇长,给你下面这样一张你们镇子的地图,每个点代表一个村子,图上的边表示村子之间的可以修建的公路,每条公路都有不同的造价,现在问你要选哪几条公路,才能使得所有村子都连通,而且造价最低?
一篇菜鸡的笔记——Prim算法详解_第1张图片
首先我们考虑一下,在保证村村通的情况下,要让这些公路造价尽量地低,那我们可以让公路尽量地少。n个村子,最少需要n-1条公路,才能达到村村通的目的。

n个顶点,n-1条边,这不就是一棵树嘛

现在的问题也就变成了,要在一张图里面,挑选图里现成的边,将所有顶点连成一棵权重最小的树
这也就是我们几天的主角——

最小生成树

先来给最小生成树下个定义:
1.何为树?——n个顶点,n-1条边,且不能有回路;
2.何为最小?——边的权重和最小;
3.何为生成 ?——结点必须是原图的所有顶点,边必须全部来源于图。

那么怎样来生成一棵树呢?

答案:种下一颗种子,然后然它慢慢长大。o( ̄▽ ̄)ブ
这就是Prim想出来的方法(所以,Prim是一个园丁喽~)

假设现在这颗树的种子是1号,为了能让这颗种子发芽,我们需要让它开枝散叶,我们把结点当作一片叶子,结点和这棵树之间的边看作是树枝。现在我们需要让它长出第一条树枝。
那第一条树枝是哪条呢?我们先来康康1号种子可以长出的树枝有哪些?
一篇菜鸡的笔记——Prim算法详解_第2张图片

1号结点相邻的边分别为4、1、2,也就是现在种子可以长出长度为4、1、2的树枝,但是我们现在想要让这棵树树枝长度和最短,所以我们当然想要权重小的边,也就是要最短的树枝,于是乎我们应该选择长出长度为1的那条树枝,而这条树枝长出来之后,叶子是4号结点。
一篇菜鸡的笔记——Prim算法详解_第3张图片
很好,现在种子发芽了,长成了一棵小树(emmmm虽然就只有一片叶子)。接下来我们想让小树继续长大,那我们应该选那条树枝呢?是继续把目光放在1号种子身上嘛???
不是!
这个时候,我们关注的是树能长出什么边,而不是这颗种子了,所以我们应该把目光着眼于这棵树上面,也就是把1号和4号看做是一个整体,看作是一棵树。我们关注的是从这棵树发散开来,可以长出哪些树枝
很明显,和这棵树相连的那些边里面最短的有两条,也就是长度为2的那两条树枝。
一篇菜鸡的笔记——Prim算法详解_第4张图片
这时候我们选哪条?好纠结啊,?,小孩子才做选择。我看这个2号长得挺眉清目秀的嘛,那就长出2号叶子!
一篇菜鸡的笔记——Prim算法详解_第5张图片
这个时候小树又长大了,它有一个1号的根(原来的种子不就是树根了嘛),还有2、4两片叶子。接下来我们再来看可以长出那条树枝。
我们发现和这棵树相连的长度最小的边是2,太好了,不用纠结了,我们直接长出这条长度为2的边。
一篇菜鸡的笔记——Prim算法详解_第6张图片
哈哈,那这样太简单了,直接不停地找最小的那条树枝不就好了嘛!
别瞎说!(╬▔皿▔)凸 (人格分裂ing)

真的只是每次张出最短的那条树枝就可以了吗???注意!!!我们这里要长出一棵树,树不能有回路
假设现在就取最短的那条边,也就是长度为3的那条树枝,如果我们让这条树枝长出来,两片叶子不就连在一起了嘛。。。
所以长度为3、4的那两条树枝是长不粗来滴。
一篇菜鸡的笔记——Prim算法详解_第7张图片
所以呢,我们下一条选择的树枝是长度为4的那一条,我们让它长出来
一篇菜鸡的笔记——Prim算法详解_第8张图片
接下来的步骤就是一毛一样的了。
和现在这棵树相连的最短边是长度为1的那条树枝,我们让它长出来
一篇菜鸡的笔记——Prim算法详解_第9张图片
和现在这棵树相连的最短边是长度为6的那条树枝,我们让它长出来
一篇菜鸡的笔记——Prim算法详解_第10张图片
这个时候你会发现,所有的结点都在这棵树里面了,我们的这棵小树也就长成我们最后要的这棵大树啦O(∩_∩)O

所以,最后这个村子需要造的公路就是下面这几条
一篇菜鸡的笔记——Prim算法详解_第11张图片
造价为16块钱~~(好便宜阀)

菜鸡分析时间

我们看到在处理最小生成树的时候,我们每次都是从所有能够长出的边里面,选择最短的那一条边,纳入,然后再选最短的,再纳入…
咦?咋看着那么眼熟?那个谁…迪克啥来着?
Dijkstra : mmp.

我们来回忆一下Dijkstra,在处理单源最短路的时候,从原点出发,我们是不是每次都纳入到源点距离最短的那个点,然后更新其他点的距离,继续选择最短的,纳入,更新…
我们会发现Prim算法和Dijkstra算法有非常相似的地方。他们都是在处理每一步的时候,选择当前最优的、最有利的、最有底的哪一种情况,然后更新其他次优的,不停循环往复,一步步直到所有次优都变成了最优,整个计划也就是最优的规划了。
这是因为他们都有一个老祖宗——

贪心算法

所以说,Prim和Dijkstra其实都是贪心算法的一种应用,只要掌握了这种基本算法的思想,其他类似的演化而来的算法看起来都会显得亲切。

下面附上Prim代码:

#include 
#include 

#define INFINITY 1000000
#define MaxVertexNum 10  /* maximum number of vertices */
typedef struct GNode *PtrToGNode;
struct GNode{
    int Nv;
    int Ne;
    WeightType G[MaxVertexNum][MaxVertexNum];
};
typedef PtrToGNode MGraph;
MGraph ReadG(void)
{
	MGraph G=(MGraph)malloc(sizeof(struct GNode));
	scanf("%d%d",&G->Nv,&G->Ne);
	int i,j;
	for(i=0;i<G->Nv;i++)
	{
		for(j=0;j<G->Nv;j++)
		{
			G->G[i][j]=INFINITY;
		}
	}
	for(i=0;i<G->Ne;i++)
	{
		int a,b,x;
		scanf("%d%d%d",&a,&b,&x);
		G->G[a][b]=x;
	}
	
	return G;
}

int Prim(int S,int *dist,int *parent,MGraph G)//小树逐渐长成大树 
{
	//先把源点纳入为树根,撒下种子 
	 dist[S]=0;
	 parent[S]=-1;
	//初始化 
	 int i;
	 for(i=1;i<G->Nv;i++)
	 {
	 	dist[i]=G->G[S][i];
	 	parent[i]=S;
	 }

	 while(1)
	 {
	 	//找出dist最小的未被纳入生成树的结点
	 	int minindex=INFINITY;
	 	int first=1;
	 	for(i=1;i<G->Nv;i++)
	 	{
	 		if(dist[i]!=0&&dist[i]!=INFINITY)
	 		{
	 			if(first==1)
	 			{
	 				minindex=i;
	 				first++; 
				}
				if(dist[i]<=dist[minindex])
				minindex=i;
			}
		}
		//树长成,则跳出循环
		if(minindex==INFINITY)break;
		//把minindex纳入最小生成树 
		dist[minindex]=0; 
		 
		//minindex的纳入,对其未被纳入的邻接点有可能产生影响
		for(i=1;i<G->Nv;i++)
		{
			if(dist[i]!=0&&G->G[mindex][i]<dist[i])
			{
				//所有和minindex邻接的未被纳入的,且dist可以被更新的点
				dist[i]= G->G[mindex][i];
				parent[i]=minindex;
			}
		} 
	 } 
	 
	 for(i=1;i<G->Nv;i++)
	 if(dist[i]==INFINITY)return 0;
	 //图不连通,无法生成最小生成树
	 
	 return 权重和; 
	 //可以生成最小生成树 
} 
int main()
{
	MGraph G= ReadG(); 
	int dist[G->Nv];
	int parent[G->Nv];
	int f=Prim(0,dist,parent,G)
	
	if(f)
	{
		//打印最小生成树 
	}
	else
	printf("此图不连通\n");
}

所谓道家修炼的的最高境界不是学“术”而是学“法”。学了一种“术”,就只能在一种特定情况下施展;但是要是融会贯通了一种“法”,所有“术”都不在话下,此便为“心法"。对于我们程序员来说,这种“心法"便是“算法”。

所以,菜鸡的我还是好好修炼叭/(ㄒoㄒ)/~~

—————————————————————————————————————————————————————————————

(・∀・(・∀・(・∀・)——(・∀・(・∀・(・∀・)——(・∀・)(・∀・(・∀・(・∀・)——(・∀・(・∀・(・∀・) 俺是一条懵逼 华丽的分割线

—————————————————————————————————————————————————————————————

Kruskal:干啥呢?干啥呢?讲到最小生成树,我都不算上???(暴躁老哥秃然上线)

前面我们讲到Prim算法,是种下一颗种子,让种子慢慢长成一棵大树。那我们可不可以换种思路,不是小树长成大树,而是小树拼成森林

Kruskal算法

怎么个小树拼成森林?首先还是前面那张图

一篇菜鸡的笔记——Prim算法详解_第12张图片

既然我们想要最小生成树,我们在意的是边的权重和最小,那我们何必把重心放到这棵树上面?我们可以直接关注边,我们直接去贪边

现在这张图里面最短的边是哪条?很明显,是长度为1的那两条边。所以我们直接把边纳入,于是现在就有了这样棵树:

一篇菜鸡的笔记——Prim算法详解_第13张图片

在接下来我们发现有一条长度为2的边,于是我们也很开心地把它纳入:
一篇菜鸡的笔记——Prim算法详解_第14张图片
在接下来我们还是找长度最短的边,我们找到了长度为3的一条边,但是,我们还是不能让树出现回路,于是3这条边被舍弃;
于是我们继续寻找,发现最小的边长度为4,于是我们把这两条边先后纳入:
一篇菜鸡的笔记——Prim算法详解_第15张图片

再下面我们找到了5,可是5还是会让树出现回路,于是舍弃。然后便是6,发现不会有回路,于是收入:
一篇菜鸡的笔记——Prim算法详解_第16张图片
这时候,7个顶点已经有了6条边,我们要的最小生成树也长好了。

我们这里还是用到了贪心算法的思想,只不过换了一种角度,你会发现Kruskal算法和Prim算法比起来显得更加直接,有一种整体视角。因为我们要的不就是边权和最小,那么在保证不产生回路的前提下,每次都纳入最小权重的边,直到所有边都被纳入或者所有结点都在生成树里。

但是Kruskal算法写起来更加复杂,主要有两点:
1.如何每次去找出最小的边。
2.如何去判断这条边是否会形成回路(树如何存放)。

解决这两个问题我们就要用到两种典型的数据结构:小顶堆(优先队列)、集合。

Kruskal伪代码:

int Kruskal(MGraph G)//几棵树并成一个森林 
{
	Set={nv个结点(集合)}//集合,用来存放最小生成树
	
	heap={初始化}//小顶堆,用来将存放结构元素,结构成员为V,W,weight,依据weight调堆 
	
	while(heap不空&&Set中还有不止一个集合) 
	{
		{V,W}=pop heap;
		//取出边V-W 
		if(边V,W不会使Set形成回路)//在集合中查找,看V、W是否在同一个集合
		Set={union V,W}
		else;
		//这条边作废 
	}
	
	if(Set中仍有不止一个集合,即原图不连通)
	return 0;
	else
	return 最小权重和; 
} 

你可能感兴趣的:(c语言实现)