真叫人头大,又来了个算法,Dijkstra还没看懂,半路又杀出个Prim( ╯□╰ )。那么Prim到底是个什么玩意儿呢(・∀・(・∀・(・∀・*)
假设你是一个镇长,给你下面这样一张你们镇子的地图,每个点代表一个村子,图上的边表示村子之间的可以修建的公路,每条公路都有不同的造价,现在问你要选哪几条公路,才能使得所有村子都连通,而且造价最低?
首先我们考虑一下,在保证村村通的情况下,要让这些公路造价尽量地低,那我们可以让公路尽量地少。n个村子,最少需要n-1条公路,才能达到村村通的目的。
n个顶点,n-1条边,这不就是一棵树嘛
现在的问题也就变成了,要在一张图里面,挑选图里现成的边,将所有顶点连成一棵权重最小的树。
这也就是我们几天的主角——
先来给最小生成树下个定义:
1.何为树?——n个顶点,n-1条边,且不能有回路;
2.何为最小?——边的权重和最小;
3.何为生成 ?——结点必须是原图的所有顶点,边必须全部来源于图。
那么怎样来生成一棵树呢?
答案:种下一颗种子,然后然它慢慢长大。o( ̄▽ ̄)ブ
这就是Prim想出来的方法(所以,Prim是一个园丁喽~)
假设现在这颗树的种子是1号,为了能让这颗种子发芽,我们需要让它开枝散叶,我们把结点当作一片叶子,结点和这棵树之间的边看作是树枝。现在我们需要让它长出第一条树枝。
那第一条树枝是哪条呢?我们先来康康1号种子可以长出的树枝有哪些?
1号结点相邻的边分别为4、1、2,也就是现在种子可以长出长度为4、1、2的树枝,但是我们现在想要让这棵树树枝长度和最短,所以我们当然想要权重小的边,也就是要最短的树枝,于是乎我们应该选择长出长度为1的那条树枝,而这条树枝长出来之后,叶子是4号结点。
很好,现在种子发芽了,长成了一棵小树(emmmm虽然就只有一片叶子)。接下来我们想让小树继续长大,那我们应该选那条树枝呢?是继续把目光放在1号种子身上嘛???
不是!
这个时候,我们关注的是树能长出什么边,而不是这颗种子了,所以我们应该把目光着眼于这棵树上面,也就是把1号和4号看做是一个整体,看作是一棵树。我们关注的是从这棵树发散开来,可以长出哪些树枝 。
很明显,和这棵树相连的那些边里面最短的有两条,也就是长度为2的那两条树枝。
这时候我们选哪条?好纠结啊,?,小孩子才做选择。我看这个2号长得挺眉清目秀的嘛,那就长出2号叶子!
这个时候小树又长大了,它有一个1号的根(原来的种子不就是树根了嘛),还有2、4两片叶子。接下来我们再来看可以长出那条树枝。
我们发现和这棵树相连的长度最小的边是2,太好了,不用纠结了,我们直接长出这条长度为2的边。
哈哈,那这样太简单了,直接不停地找最小的那条树枝不就好了嘛!
别瞎说!(╬▔皿▔)凸 (人格分裂ing)
真的只是每次张出最短的那条树枝就可以了吗???注意!!!我们这里要长出一棵树,树不能有回路!
假设现在就取最短的那条边,也就是长度为3的那条树枝,如果我们让这条树枝长出来,两片叶子不就连在一起了嘛。。。
所以长度为3、4的那两条树枝是长不粗来滴。
所以呢,我们下一条选择的树枝是长度为4的那一条,我们让它长出来
接下来的步骤就是一毛一样的了。
和现在这棵树相连的最短边是长度为1的那条树枝,我们让它长出来
和现在这棵树相连的最短边是长度为6的那条树枝,我们让它长出来
这个时候你会发现,所有的结点都在这棵树里面了,我们的这棵小树也就长成我们最后要的这棵大树啦O(∩_∩)O
所以,最后这个村子需要造的公路就是下面这几条
造价为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算法,是种下一颗种子,让种子慢慢长成一棵大树。那我们可不可以换种思路,不是小树长成大树,而是小树拼成森林
怎么个小树拼成森林?首先还是前面那张图
既然我们想要最小生成树,我们在意的是边的权重和最小,那我们何必把重心放到这棵树上面?我们可以直接关注边,我们直接去贪边。
现在这张图里面最短的边是哪条?很明显,是长度为1的那两条边。所以我们直接把边纳入,于是现在就有了这样两棵树:
在接下来我们发现有一条长度为2的边,于是我们也很开心地把它纳入:
在接下来我们还是找长度最短的边,我们找到了长度为3的一条边,但是,我们还是不能让树出现回路,于是3这条边被舍弃;
于是我们继续寻找,发现最小的边长度为4,于是我们把这两条边先后纳入:
再下面我们找到了5,可是5还是会让树出现回路,于是舍弃。然后便是6,发现不会有回路,于是收入:
这时候,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 最小权重和;
}