解决最短路径问题有几个出名的算法:
1.dijkstra算法,最经典的单源最短路径算法
2.bellman-ford算法,允许负权边的单源最短路径算法
3.spfa,其实是bellman-ford+队列优化,其实和bfs的关系更密一点
4.floyd算法,经典的多源最短路径算法
1、floyd算法
1.1、推导过程:
暑假,小哼准备去一些城市旅游。有些城市之间有公路,有些城市之间则没有,如下图。为了节省经费以及方便计划旅程,小哼希望在出发之前知道任意两个城市之前的最短路程。
上图中有4个城市8条公路,公路上的数字表示这条公路的长短。请注意这些公路是单向的。我们现在需要求任意两个城市之间的最短路程,也就是求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。
现在需要一个数据结构来存储图的信息,我们仍然可以用一个4*4的矩阵(二维数组e)来存储。比如1号城市到2号城市的路程为2,则设e[1][2]的值为2。2号城市无法到达4号城市,则设置e[2][4]的值为∞。另外此处约定一个城市自己是到自己的也是0,例如e[1][1]为0,具体如下。
我们来想一想,根据我们以往的经验,如果要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。那么这个中转的顶点k是1~n中的哪个点呢?甚至有时候不只通过一个点,而是经过两个点或者更多点中转会更短,即a->k1->k2b->或者a->k1->k2…->k->i…->b。比如上图中从4号城市到3号城市(4->3)的路程e[4][3]原本是12。如果只通过1号城市中转(4->1->3),路程将缩短为11(e[4][1]+e[1][3]=5+6=11)。其实1号城市到3号城市也可以通过2号城市中转,使得1号到3号城市的路程缩短为5(e[1][2]+e[2][3]=2+3=5)。所以如果同时经过1号和2号两个城市中转的话,从4号城市到3号城市的路程会进一步缩短为10。通过这个的例子,我们发现每个顶点都有可能使得另外两个顶点之间的路程变短。好,下面我们将这个问题一般化。
假如现在只允许经过1号顶点,求任意两点之间的最短路程,应该如何求呢?只需判断e[i][1]+e[1][j]是否比e[i][j]要小即可。e[i][j]表示的是从i号顶点到j号顶点之间的路程。e[i][1]+e[1][j]表示的是从i号顶点先到1号顶点,再从1号顶点到j号顶点的路程之和。其中i是1~n循环,j也是1~n循环,代码实现如下。
for (i = 1; i <= n; i++)
{for (j = 1; j <= n; j++)
{ if (e[i][j] > e[i][1] + e[1][j]) e[i][j] = e[i][1] + e[1][j];
} }//在只允许经过1号顶点的情况下,任意两点之间的最短路程更新为:
接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短路程。如何做呢?我们需要在只允许经过1号顶点时任意两点的最短路程的结果下,再判断如果经过2号顶点是否可以使得i号顶点到j号顶点之间的路程变得更短。即判断e[i][2]+e[2][j]是否比e[i][j]要小,代码实现为如下。
//经过1号顶点
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if (e[i][j] > e[i][1]+e[1][j]) e[i][j]=e[i][1]+e[1][j];
//经过2号顶点
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if (e[i][j] > e[i][2]+e[2][j]) e[i][j]=e[i][2]+e[2][j];
在只允许经过1和2号顶点的情况下,任意两点之间的最短路程更新为:
最后允许通过所有顶点作为中转,任意两点之间最终的最短路程为:
整个算法过程虽然说起来很麻烦,但是代码实现却非常简单,核心代码只有五行:
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(ad[j][k]+ad[k][i]>0&&e[i][j]>e[i][k]+e[k][j])
//注意!两个INT_MAX相加的结果是负数。一定是小的,所以先判断是否溢出了!这里有一个溢出的问题!!!
e[i][j]=e[i][k]+e[k][j];
转载自:https://www.cnblogs.com/wangyuliang/p/9216365.html
1.2、核心分析
floyd的最关键的地方是它的递推公式,它的递推公式写得抽象一点就是下图:
简单来说,这个i到j的最短路径,我们可以找一个中间点k,然后变成子问题,i到k的最短路径和k到j的最短路径.。也就是说,我们可以枚举中间点k,找到最小的d[i][k]+d[k][j],作为d[i][j]的最小值。这好像很合理啊,假如所有d[i][k]和d[k][j]都取了最小值的话,这个dp很dp.
但是,d[i][k]和d[k][j]一开始都不一定取了最小值的啊!它们和d[i][j]一样,会不断变小.。那么,会不会存在这种情况,d[i][j]取最小值时的k是某个x.。而在最外循环k=x的时候,d[i][x]或者d[x][j]并没有取到最小值,但这个时候会执行d[i][j]=min(d[i][j],d[i][x]+d[x][j]),造成了d[i][j]并不能取到真正的最小值.。答案当然是,并不会出现这种情况.我们今天的重点就是来讨论为什么不会出现这种情况.
推导:对于1->3如果我们认为1->4+4->3是最短路径。那么是否存在一个6使得1->变得更小的?
那么我们想证明的就是1->4是不可以更改的。也就是1->4<1->6+6->4是假的。
首先可以得到公式0:1->4+4->3=1->3
而且我们知道如果1->3是最小的,那么很明显公式1:1->3<=1->6+6->3(显然,不然4就不是最短路径的中转了,到6的时候又更新了)
那么又知道公式2:6->3<=6->4+4->3(等于的情况就是 在K=4的时候,及可以经过4的时候 6->4+4->3是比原始的6->3小的或者等的,那么6->3更新为6->4+4->3。 大于的情况是,在K=5的时候又一次更新了,那么当前的6->3其实是经过5的结果。那么当前的6->3肯定是要比6->4+4->3小的。还有一种情况就是在第K=4的时候本身6->3<=6->4+4->3就成立,那么6->3就不改)
将公式1、2整合得到公式3:1->3<=1->6+6->3<=6->4+4->3+1->6
将公式3、0整合得到:1->4+4->3=1->3<=1->6+6->3<=6->4+4->3+1->6
化简得到:1->4<=6->4+1->6 与1->4<1->6+6->4相悖。所以可以发现:不会出现你觉得a[i][j]通过a[i][k]+a[k][j]得到了最终的最小值之后。a[i][k]又变小了的情况
另外需要注意的是:正常的Floyd算法不能解决带有“负权回路”(或者叫“负权环”)的图,因为带有“负权回路”的图没有最短路。例如下面这个图就不存在1号顶点到3号顶点的最短路径。因为1->2->3->1->2->3->…->1->2->3这样路径中,每绕一次1->-2>3这样的环,最短路就会减少1,永远找不到最短路。其实如果一个图中带有“负权回路”那么这个图则没有最短路。
2、Dijkstra算法
1.1推导过程
1)算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
2)算法步骤:
a.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。
b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
d.重复步骤b和c直到所有顶点都包含在S中。
1.2 代码
int* Dijkstra(int now,int ad[][6])
{
const int num=6;
for(int i=0;i<num;i++) ad[i][i]=0;
static int vis[num],dis[num];
memset(vis,0,sizeof(vis));//vis数组清空
for(int i=0;i<num;i++)
{dis[i]=ad[i][now];}//从map第一列开始将值存到dis数组中,dis数组存放最短路径权值
vis[now]=1;//将vis数组置的n结点权值为1 ,标记数组,代表已经访问过
//两重for循环找最短路径
for(int i=0;i<num;i++)
{
int minlen=INT_MAX,temp;
for(int j=0;j<num;j++)
{
if(!vis[j]&&dis[j]<minlen)//没有访问过该节点,且存放最短路径的dis数组在该点权值小于minn
{
minlen=dis[j];//将dis数组在该点的权值更新到minn变量中保存
temp=j;
}
}//找当前没访问过的那些位置里,dist最小的那个位置。把那个位置放进遍历过的集合里。及将那个位置标志为访问过的。
vis[temp]=1;//标记已访问过该点
for(int k=0;k<num;k++)//之后与folyd相似,当允许这个点进行中转的时候,更新矩阵。(而这个刚被允许的点,由她进行中转不会更改之前的那些更小的点。因为那些点到目标点本身就比这个点到目标点小)
if(ad[k][temp]+dis[temp]>0 && dis[k]>ad[k][temp]+dis[temp])//但是注意不同的是,这里不再是看任意两个位置之间的距离,而是看与now之间的距离,还要注意temp与now的距离不是ad[temp][now]而是dis[temp]
dis[k]=ad[k][temp]+dis[temp];//如果当前结点的权值+存放在最短路径的当前结点的权值累加和 小于 前一个结点的权值,则更新前一个结点的权值
}
for(int i=0;i<num;i++)cout<<dis[i]<<" ";
cout<<endl;
return dis;}
3、prim算法
在一个无向连通图中,如果存在一个连通子图包含原图中所有的结点和部分边,且这个子图不存在回路,那么我们称这个子图为原图的一棵生成树。在带权图中,所有的生成树中边权的和最小的那棵(或几棵)被称为最小生成树。
kruskal
算法的过程为不断对子图进行合并,直到形成最终的最小生成树。prim
算法的过程则是只存在一个子图,不断选择顶点加入到该子图中,即通过对子图进行扩张,直到形成最终的最小生成树。
扩张过程中选择的顶点,是距离子图最近的顶点,即与子图中顶点形成的边是权值最小的边。
算法过程
- 按照距离子图的远近,对顶点集合进行排序
- 选择最近的顶点加入到子图中,并更新相邻顶点对子图的距离
- 重复执行步骤 2,直到顶点集合为空
void prim(int ad[][6])
{
const int num=6;
for(int i=0;i<num;i++) ad[i][i]=0;
int now=0;//从第0个点开始
int visited[num],dis[num];
memset(visited,0,sizeof(visited));
visited[now]=1;
for(int i=0;i<num;i++) dis[i]=ad[i][now];
for(int ii=1;ii<num;ii++)
{
int minl=INT_MAX,temp=-1,i;
for(i=0;i<num;i++)
if(!visited[i]&&dis[i]<minl)
{
minl=dis[i];
temp=i;
}
if(temp==-1) continue;
visited[temp]=1;
for(int j=0;j<num;j++)
{
if(dis[j]>ad[temp][j]+dis[temp])
dis[j]=ad[temp][j]+dis[temp];
} }
for(int i=0;i<num;i++) cout<<dis[i]<<" ";
//得到的其实是到点now的最短路径,如果所有最短路径都小于INT_MAX,说明是连通的
}
4、kruskal算法
4.1、推导
(1)初始时所有结点属于孤立的集合。
(2)按照边权递增顺序遍历所有的边,若遍历到的边两个顶点仍分属不同的集合(该边即为联通这两个集合的边中权值最小的那条)则确定该边为最小生成树上的一条边,并将这两个顶点分属的集合合并。
(3)遍历完所有边后,原图上所有结点属于同一个集合则被选取的边和原图中所有结点构成最小生成树;否则原图不连通,最小生成树不存在。
struct edge{
int a,b,cost;
edge(int a1,int a2,int a3)
{a=a1;b=a2;cost=a3;}
};
bool cmp(edge* a,edge* b){
return a->cost<b->cost;
}
int findroot(int now,int* tree){
if(tree[now]==-1) return now;//说明要么这个点没遍历过要不这个点是root点
else{
int temp=findroot(tree[now],tree);//可能存在刚合并完。当前的now点的tree还是之前的root而不是合并之后的root。所以要再find一下之后更新一下
tree[now]=temp;
return temp;
}
}
void kruskal(int ad[][6])
{
const int num=6;
for(int i=0;i<num;i++) ad[i][i]=0;
vector<edge*> bian;
int tree[num];
for(int i=0;i<num;i++) tree[i]=-1;
for(int i=0;i<num;i++)
for(int j=i;j<num;j++)
{ if(ad[i][j]!=2139062143)
{
edge* k=new edge(i,j,ad[i][j]);
bian.push_back(k);
} }
sort(bian.begin(),bian.end(),cmp);
int ans=0;
for(int i=0;i<bian.size();i++)//相同的方法可以做并查集。
{
int a=findroot(bian[i]->a,tree);
int b=findroot(bian[i]->b,tree);//相同的集合是使用的相同的root点。
if(a!=b) //对于一个边上的两个点隶属于两个集合,那么这两个集合可以混合。代表着这个点a的root可以是b的root
{
tree[a]=b;
ans+=bian[i]->cost;
}
}
for(int i=0;i<num;i++)
cout<<tree[i]<<" ";
cout<<ans;//ans为最小生成树的长度。可以使用kruskal算法得到最小生成树连通中的长度。
}