无向图中的最短路径问题?No,最短路径不是最小生成树!
什么是最小生成树?
在一个无向连通图中,有一个子图连接所有顶点,并且权重和最小,那么他就是最小生成树。如果权重和不是最小的只能叫做生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。
如果各位学过最短路径(最短路径(Floyd-Warshall、Dijkstra、Bellman-Ford)-CSDN博客),那么对学两个算法就会很有一点帮助,但正如前边所说,最短路径并非最小生成树,二者是由区别的。这两个算法采用的是逐步求解的贪心策略。
我们先来看一道例题:
镖局运镖(《啊哈!算法》)
最近小哼迷上了《龙门镖局》,从恰可图到武夷山,从张家口到老河口,从迪化到佛山,从蒙自到奉天,迤逦数千里的商道上,或牛马,或舟楫,或驼驮(tuó),或肩挑,货物往来,钱财递送,皆离不开镖局押运。商号开在哪里,镖局便设在哪里。古代镖局的运镖,就是运货,也就是现代物流,镖局每到一个新地方开展业务,都需要对运镖途中的绿林好汉进行打点。好说话的打点费就比较低,不好说话的打点费就比较高。现在已知城镇地图如下,顶点是城镇编号,边上的值表示这条道路上打点绿林好汉需要的银子数。
镖局现在需要选择一些道路进行疏通,一遍镖局可以达到任意一个城镇,要求是花费的银子越少越好。
输入格式:
输入m+1行,第一行两个数n和m,n表示n个城市,m表示m条道路。接下来的m行,每行形如“a b c”,用来表示一条道路,意思是城市a到城市b需要花费的银子数是c。
输出格式:
输出一行,一个数表示花费的最少银子数。
输入样例:
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
输出样例:
19
这道题其实就是求这个无向连通图的最小生成树,首先要保证的是最短,这道题用贪心策略可以求得最优解,我们可以证明贪心是可行的,那么我们就是求任意两个顶点之间的最小权重路径,树不需要有回路,但并没有限制每个顶点连几条边。
Kruskal算法
任意两个顶点之间的最小权重路径,Kruskal的做法是将路径从小到大排序,选择最小并不会产生回路的。因为我们要得到的是最小生成树,保证的是权重和最小,而不是单源最短路径最小。
举个例子:我们求得的最小生成树为红色线所示,但1->3的最短路径不包含在其中。
我们将所有边排序:
1 2 1
1 3 2
4 6 3
5 6 4
2 3 6
4 5 7
3 4 9
2 4 11
3 5 13
然后就是从小到大选择:
当选择到“2 3 6”,这条边时,发现会形成回路,故跳过这一个:
最终选用“3 4 9”这条边,形成最小生成树:
那么我们如何判断是否会形成回路呢?可以通过book标记已经走过的数组嘛?答案是肯定不行的。这是无向图,我们没有向Prim一样确定从哪个顶点到哪个顶点,如果用book标记一条边的两个顶点的话,便不可能形成连通的子图了。
所以我们使用并查集(初识树(二叉树,堆,并查集)-CSDN博客)的方法。“u v w”中,如果u和v属于一个集合,那么连通这条路就会形成回路。
所以Kruskal主要用到两个算法:一个是排序,一个是并查集,我们这里采用快排。当已经连通n-1条边的时候,就代表最小生成树已经形成。
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f
struct edge
{
int u;
int v;
int w;
};/*方便排序,用结构体存*/
int find(int v,int *f)/*查找*/
{
if (f[v]==v)
{
return v;
}
else
{
f[v]=find(f[v],f);/*路径压缩*/
return f[v];
}
}
int Union(int x,int y,int *f)/*合并*/
{
int t1=find(x,f);
int t2=find(y,f);
if (t1!=t2)
{
f[t2]=t1;
return 1;
}
return 0;
}
void quicksort(int left,int right,struct edge *e)/*快排*/
{
int i,j;
struct edge t;
if (left>right)
{
return ;
}
i=left;
j=right;
while (i!=j)
{
while (e[j].w>=e[left].w && i<j)
j--;
while (e[i].w<=e[left].w && i<j)
i++;
if (i<j)
{
t=e[i];
e[i]=e[j];
e[j]=t;
}
}
t=e[left];
e[left]=e[i];
e[i]=t;
quicksort(left,i-1,e);
quicksort(i+1,right,e);
return ;
}
int main()
{
struct edge e[20];
int n,m;
scanf("%d%d",&n,&m);
for (int i = 1; i <= m; i++)
scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
// printf("test\n");
quicksort(1,m,e);
// for (int i = 1; i <= m; i++)/*打印排序过的路径*/
// printf("%d %d %d\n",e[i].u,e[i].v,e[i].w);
int f[20];
for (int i = 0; i <= n; i++)/*并查集初始化*/
f[i]=i;
int cont=0,sum=0;
for (int i = 1; i <= m; i++)
{
if (Union(e[i].u,e[i].v,f))/*判断是否会形成回路*/
{
cont++;
sum+=e[i].w;
}
if (cont == n-1)/*已经连通n-1条边*/
{
break;
}
}
printf("sum=%d",sum);
return 0;
}
Prim算法
它与Dijkstra简直不能太相似了,区别就是在松弛的时候,Dijkstra是判断单源顶点到其它点的最短距离,而Prim是最小生成树(把最小生成树看成一个结点)到其它点的最短距离。
还是用上边那个图作为例子:
在连接3顶点的时候,是选择紫线还是绿线呢?如果求最短路径,到1顶点的距离是6和7,那当然是选紫线,但是现在是最小生成树,二者到3结点的距离是6和7,那么就是选绿线了。
本代码结合了,邻接表,最小堆,代码比较长,但时间复杂度大大降低到了,如果用邻接矩阵和遍历的方法,时间复杂度为:。故我们这里只写时间复杂度小的代码:
这里不再写程序设计部分,因为和Dijkstra差不多,会稍微讲一下堆优化。
完整代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f
int h[10]={0};/*h用来保存堆*/
int pos[10]={0};/*pos用来存储每个顶点在堆中的位置*/
void swap(int x,int y)/*交换函数,用来交换堆中的两个元素的值*/
{
int t;
t=h[x];
h[x]=h[y];
h[y]=t;
/*同步更新pos*/
t=pos[h[x]];
pos[h[x]]=pos[h[y]];
pos[h[y]]=t;
return ;
}
void siftdown(int *num,int i,int n)/*向下调整函数,形成最小堆*/
{
int t=0,flag=0;
while (flag==0&&2*i<=n)
{
if (num[h[i]]>num[h[i*2]])/*左儿子小于于父亲*/
{
t=2*i;
}
else
t=i;
if (2*i+1<n&&num[h[t]]>num[h[2*i+1]])/*右儿子小于t结点*/
{
t=2*i+1;
}
if (t!=i)
{
swap(t,i);
i=t;
}
else
flag=1;
}
return ;
}
void siftup(int *num,int i)/*向上调整函数,形成最大堆*/
{
int t=0,flag=0;
if (i==1)
{
return ;
}
while (flag==0&&i!=1)
{
if (num[h[i]]<num[h[i/2]])/*父结点大于子结点,交换*/
{
t=i/2;
swap(i,t);
}
else
flag=1;
i=i/2;
}
}
int pop(int n,int *num)/*将堆顶出堆*/
{
int t;
t=h[1];/*临时变量存储堆顶对应的图中的结点*/
pos[t]=0;/*无所谓*/
h[1]=h[n];/*将堆中的最后一个结点赋给堆顶*/
pos[h[1]]=1;/*更新pos*/
n--;/*堆元素减1,表示又排序了一个*/
siftdown(num,1,n);/*将堆顶调整,形成新的最小堆*/
return t;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int n_flag=n;
/*u,v,w,next的数组大小要根据实际情况来确定,此图为无向图,要比2*m大1*/
/*first要比n大1*/
int u[20]={0},v[20]={0},w[20]={0},first[20],next[20]={0};
memset(first,-1,sizeof(int)*(n+1));/*初始化first*/
int dis[20]={0};
memset(dis,inf,sizeof(int)*(n+1));/*初始化 dis*/
int k=0;
for (int i = 1; i <= m ; i++)
{
scanf("%d%d%d",&u[i],&v[i],&w[i]);
}
for (int i = m+1; i <= 2*m; i++)
{
u[i]=v[i-m];
v[i]=u[i-m];
w[i]=w[i-m];
}
for (int i = 1; i <= 2*m; i++)/*创建邻接表,无向图*/
{
next[i]=first[u[i]];
first[u[i]]=i;
}
for (int i = 0; i <= n; i++)
{
h[i]=i;
pos[i]=i;
}
k=first[1];
dis[1]=0;
while(k!=-1)/*初始化dis数组,到生成树的最短距离*/
{
dis[v[k]]=w[k];
k=next[k];
}
for (int i = n/2; i >= 1; i--)/*创建最小堆*/
{
siftdown(dis,i,n);
}
pop(n,dis);/*先弹出dis[1]=0这一个*/
n--;/*堆顶元素减1,函数里面是形参,这里可以用全局变量改进*/
/*核心代码*/
int book[10]={0};/*保证不能有回路*/
int cont=0;/*统计生成树有几个结点*/
book[1]=1;
cont++;
int sum=0;
while (cont<n_flag)
{
int j=pop(n,dis);
// printf("j=%d h[1]=%d dis[h[i]]=%d\n",j,h[1],dis[h[1]]);/* 堆排序查看 */
n--;
cont++;
book[j]=1;
sum+=dis[j];
k=first[j];
while (k!=-1)
{
if (dis[v[k]]>w[k]&&book[v[k]]==0)
{
// printf(" %d %d %d pos[v[k]]=%d\n",u[k],v[k],w[k],pos[v[k]]);/*邻接表查看*/
dis[v[k]]=w[k];
siftup(dis,pos[v[k]]);
}
k=next[k];
}
}
printf("sum=%d\n",sum);
/******* */
return 0;
}
堆的优化讲解:
这里我们需要三个数组来实现堆优化:dis、h、pos。
我们来看一个刚开始生成的最小堆,即最小生成树只有1结点的时候,如图:
堆里面并没有直接结点存储权重,因为树中结点的位置,与图中顶点不同,树中的1结点并不一定是无向连通图中的1结点。 故我们这里用到了p和pos两个数组来表示他们的对应关系:
在p数组中,下标对应堆中的结点,值对应图中的顶点。
在pos数组中,下标对应图中的顶点,值对应堆中的结点。
dis数组中,下标表示图中的顶点,值对应,顶点到最小生成树的最短路径。
总结
如果所有的边权不相等,那么最小生成树就是唯一的,Kruskal算法是一步步将树林中的树合并,而prim算法则是通过每次增加一条边来建立树。Kruskal算法适用于稀疏图,没有使用堆优化的prim适用于稠密图,使用了堆优化的Prim适用于稀疏图。
参考文献
《啊哈!算法》
图详解第三篇:最小生成树(Kruskal算法+Prim算法)-腾讯云开发者社区-腾讯云
最小生成树详细讲解(Prime算法+Kruskalsuanfa)_牛客博客