最短路径

来源:互联网 发布:c语言是c 的基础吗 编辑:程序博客网 时间:2024/06/10 18:15

最短路径

有四种方法Floyddijkstrabellman_fordSPFA算法;(最后两个可以判断是否具有负权回路)

方1Floyd(或弗洛伊德)

目的:

寻找给定的加权图中多源点之间最短路径。

原理:

利用动态规划的思想。即,通过一个图的权值矩阵求出它的每两点间的最短路径矩阵。

间复:O(n^3);

核心代码:

for(k=1;k<=n;k++)

   for(i=1;i<=n;i++)

for(j=1;j<=n;j++)

if(mp[i][j]>mp[i][k]+mp[k][p])

mp[i][j]=mp[i][k]+mp[k][p];

k:中间转点;

思路:利用三重循环;一次打开中间转点;for(k=1;k<=n;k++);

那我每打开一个中间转点,我就把我的地图遍历一遍,把可以连通的点的值加以更新,最终我们把所有点都打开了,就可以获得任意两个点之间的最短距离;

注:

这里值得我们注意的是在初始化时,mp地图应该被初始为INF,(INF指无限大);#define INF 100000000

记住不要用memset(mp,INF,sizeof(mp));值不稳定!

应该是:for(i=1;i<=n;i++)

          for(j=1;j<=n;j++)

if(i==j)mp[i][j]=0;

else

             mp[i][j]=INF;

方2dijkstra迪杰斯特拉算法(或狄克斯特拉算法)

目的:是从一个顶点到其余各顶点的最短路径.

原理:用一维数组建立1~n之间的最短路径.

时间复杂度o(n^2);

思路:首先我要清楚我需要用到哪些成员:

vis[]:判断是否访问过顶点;

mp[][]:记录地图的,即建立各点之间的关系;

p:记录我找符合条件的所在点;目的:再继续比较目标层与dis[]的关系;

一开始,我就把1号与各点的长度依次赋值给dis[],然后我通过找到与1号点最近的点,然后对最近的点继续找下去看看需不需要更新1-n点长度,如果当前点j比(前驱点+已确定的值)要大,则我们更新dis[]所在1~j之间的长度;

当然我们每确定1~k那个k的顶点时,我们就标记一下,以后不再访问;

 

核心代码:

n:n个顶点

for(i=1;i<=n;i++)

dis[i]=map[1][i];//dis[]含义是指1号点到各点的路径长;

for(i=1;i<=n;i++)

{

  min=INF;

  for(j=1;j<=n;j++)

  {

   if(!vis[j]&&min>dis[j])//查找1号点到各点的最小值;

   {

min=dis[j];

p=j;

    }

   }

  vis[p]=1;

   for(k=1;k<=n;k++)

if(dis[k]>mp[p][k]+dis[p]&&mp[p][k]<INF)

     dis[k]=mp[p][k]+dis[p];//继续找1号对应的最小权的点,所对应的层的值,一次对比一下,判断是否更新dis的值,到达松弛的效果;

  }

}

一共我需要循环n次,dis被更新的次数最多是n-1次;

方3Bellman– ford(或贝尔曼-福特算法)

目的:求含负权图的单源最短路径。

原理:

连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。

思路:找与它有联系的点且联系点已确定,比较与源点的距离大小,比本来的小就更新;

核心代码:

for(i=1;i<=n;i++)

scanf(“%d %d %d”,&u[i],&v[i],&w[i]);

for(k=1;k<n;k++)//n:点数

{

 check=0;//检查是否更新;

 for(j=1;j<=m;j++)//m边数

if(dis[v[j]]>dis[u[j]]+w[j])

{

  dis[v[j]]=dis[u][j]]+w[j];

check=1;

}

if(check==0)break;//没有更新,就直接跳出;

}

下面我们检查负回权路

int flag=0;

for(i=1;i<=m;i++)

{

  if(dis[v[i]]>dis[u[i]]+w[i])//表示更新n-1次之后我还能继续更新,还有最小值,就不符合有最短路的情况;so,是负权回路!

{

   flag=1;//标记有负权回路;

}

}

方四:SPFA算法:

目的:求距离源点的最短路径。同时SPFA可以处理负权边。

原理:运用队列,首先找到源点,将它入列,同时我还需要标记vis[u]u这个点是否在列中,我先找到离源点最近的点,比较我已经确定的(亦可称为前驱点)+前驱点与该点边权值与该点本来具有的值相比较,然后更新dis[],使dis[]松弛到最小;

基本步骤:

初始化+松弛

初始化: d数组全部赋值为INF(无穷大);p数组全部赋值为s(即源点),或者赋值为-1,表示还没有知道前驱

        然后d[s]=0;  表示源点不用求最短路径,或者说最短路就是0。将源点入队;

顶点入队标记vis数组,有顶点出队消除标记;

思路:

用dis[]数组记录每个结点的最短路径估计值,用邻接链表储存图,主要采用的思想是动态逼近法,运用队列保存待优化的结点,优化时每次取队首结点u,并且u点当前的最短路径估计值对离开u点所指向的结点的v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾,就是继续不断进行取队首,然后找到与其相匹配的最短路径点,判断是否need更新,同时还要标记是否在队列中;

主要元素:

队列+松弛

读取队头顶点u,并将队头顶点u出队(记得消除标记);将与点u相连的所有点v进行松弛操作,如果能更新估计值(即令d[v]变小),那么就更新,另外,如果点v没有在队列中,那么要将点v入队(记得标记),如果已经在队列中了,那么就不用入队

以此循环,直到队空为止就完成了单源最短路的求解

网上证明:

  每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。(证毕)

期望的时间复杂度O(ke)其中k为所有顶点进队的平均次数,可以证明k一般小于等于2

判断有无负环:

如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)

代码实现:

bfs_SPFA:

int spfa_bfs(int s)

{

    queue <int> q;

    memset(d,INF,sizeof(d));

    d[s]=0;

    memset(cnt,0,sizeof(cnt));

    memset(vis,0,sizeof(vis));

    q.push(s); vis[s]=1; cnt[s]=1;

    //顶点入队vis要做标记,另外要统计顶点的入队次数

    int OK=1;

    while(!q.empty())

    {

        int x;

        x=q.front(); q.pop();  vis[x]=0;

        //队头元素出队,并且消除标记

        for(int k=f[x]; k!=0; k=nnext[k]) //遍历顶点x的邻接表

        {

            int y=v[k];

            if( d[x]+w[k] < d[y])

            {

                d[y]=d[x]+w[k];  //松弛

                if(!vis[y])  //顶点y不在队内

                {

                    vis[y]=1;    //标记

                    c[y]++;      //统计次数

                    q.push(y);   //入队

                    if(c[y]>NN)  //超过入队次数上限,说明有负环

                        return OK=0;

                }

            }

        }

    }

 

    return OK;

}

dfs_SPFA:

int spfa_dfs(int u)

{

    vis[u]=1;

    for(int k=f[u]; k!=0; k=e[k].next)

    {

        int v=e[k].v,w=e[k].w;

        if( d[u]+w < d[v] )

        {

            d[v]=d[u]+w;

            if(!vis[v])

            {

                if(spfa_dfs(v))

                    return 1;

            }

            else

                return 1;

        }

    }

    vis[u]=0;

    return 0;

}

最小生成树:

构成最小生成树条件:

1).尽可能去权值小的边,但不可构成回路;

2).选取N-1条边连通N个顶点;

prim算法适合稠密图,其时间复杂度为O(n^2),其时间复杂度与边得数目无关,而kruskal算法的时间复杂度为O(eloge)跟边的数目有关,适合稀疏图。

最小生成树之prim算法

边赋以权值的图称为网或带权图,带权图的生成树也是带权的,生成树T各边的权值总和称为该树的权。

   最小生成树(MST):权值最小的生成树。

   生成树和最小生成树的应用:要连通n个城市需要n-1条边线路。可以把边上的权值解释为线路的造价。则最小生成树表示使其造价最小的生成树。

   构造网的最小生成树必须解决下面两个问题:

    1、尽可能选取权值小的边,但不能构成回路;

    2、选取n-1条恰当的边以连通n个顶点;

    MST性质:假设G=(V,E)是一个连通网,U是顶点V的一个非空子集。若(u,v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树。

1.prim算法

  基本思想:假设G=(V,E)是连通的,TE是G上最小生成树中边的集合。算法从U={u0}(u0∈V)、TE={}开始。重复执行下列操作:

   在所有u∈U,v∈V-U的边(u,v)∈E中找一条权值最小的边(u0,v0)并入集合TE中,同时v0并入U,直到V=U为止。

   此时,TE中必有n-1条边,T=(V,TE)为G的最小生成树。

   Prim算法的核心:始终保持TE中的边集构成一棵生成树。

注意:prim算法适合稠密图,其时间复杂度为O(n^2),其时间复杂度与边得数目无关,而kruskal算法的时间复杂度为O(eloge)跟边的数目有关,适合稀疏图。

看了上面一大段文字是不是感觉有点晕啊,为了更好理解我在这里举一个例子,示例如下:

 

 

(1)图中有6个顶点v1-v6,每条边的边权值都在图上;在进行prim算法时,我先随意选择一个顶点作为起始点,当然我们一般选择v1作为起始点,好,现在我们设U集合为当前所找到最小生成树里面的顶点,TE集合为所找到的边,现在状态如下:

U={v1}; TE={};

(2)现在查找一个顶点在U集合中,另一个顶点在V-U集合中的最小权值,如下图,在红线相交的线上找最小值。

 

通过图中我们可以看到边v1-v3的权值最小为1,那么将v3加入到U集合,(v1,v3)加入到TE,状态如下:

U={v1,v3}; TE={(v1,v3)};

(3)继续寻找,现在状态为U={v1,v3}; TE={(v1,v3)};在与红线相交的边上查找最小值。

 

我们可以找到最小的权值为(v3,v6)=4,那么我们将v6加入到U集合,并将最小边加入到TE集合,那么加入后状态如下:

U={v1,v3,v6}; TE={(v1,v3),(v3,v6)}; 如此循环一下直到找到所有顶点为止。

(4)下图像我们展示了全部的查找过程:

 

2.prim算法程序设计

(1)由于最小生成树包含每个顶点,那么顶点的选中与否就可以直接用一个数组来标记used[max_vertexes];(我们这里直接使用程序代码中的变量定义,这样也易于理解);当选中一个数组的时候那么就标记,现在就有一个问题,怎么来选择最小权值边,注意这里最小权值边是有限制的,边的一个顶点一定在已选顶点中,另一个顶点当然就是在未选顶点集合中了。我最初的一个想法就是穷搜了,就是在一个集合中选择一个顶点,来查找到另一个集合中的最小值,这样虽然很易于理解,但是很明显效率不是很高,在严蔚敏的《数据结构》上提供了一种比较好的方法来解决:设置两个辅助数组lowcost[max_vertexes]和closeset[max_vertexes],lowcost[max_vertexes]数组记录从U到V-U具有最小代价的边。对于每个顶点v∈V-U,closedge[v], closeset[max_vertexes]记录了该边依附的在U中的顶点。

注意:我们在考虑两个顶点无关联的时候设为一个infinity 1000000最大值。

说了这么多,感觉有点罗嗦,还是发扬原来的风格举一个例子来说明,示例如下:

 

过程如下表:顶点标号都比图中的小1,比如v1为0,v2为1,这里首先选择v1点。

Lowcost[0]

Lowcost[1]

Lowcost[2]

Lowcost[3]

Lowcost[4]

Lowcost[5]

U

V-U

closeset

v1,infinity

v1,6

v1,1

v1,5

v1,infinity

v1,infinity

v1

v1,v2,v3,v4,v5,v6

从这个表格可以看到依附到v1顶点的v3的Lowcost最小为1,那么选择v3,选择了之后我们必须要更新Lowcost数组的值,因为记录从U到V-U具有最小代价的边,加入之后就会改变。这里更新Lowcost和更新closeset数组可能有点难理解,

 for (k=1;k<vcount;k++)

            if(!used[k]&&(G[j][k]<lowcost[k]))

                { lowcost[k]=G[j][k];

                closeset[k]=j; }

        }

j为我们已经选出来的顶点,如果G[j][k]<lowcost[k],则意味着最小权值边发生变化,更新该顶点的最小lowcost权值,依附的顶点肯定就是刚刚选出的顶点j,closeset[k]=j。

Lowcost[0]

Lowcost[1]

Lowcost[2]

Lowcost[3]

Lowcost[4]

Lowcost[5]

U

V-U

closeset

v1,infinity

v1,6

v1,1

v1,5

v3,6

v3,4

v1,v3

v1,v2,v4,v5,v6

这样一直选择下去直到选出所有的顶点。

(2)上面把查找最小权值的边结束了,但是这里有一个问题,就是我们没有存储找到的边,如果要求你输出找到的边那么这个程序就需要改进了,我们刚开始的时候选取的是v1作为第一个选择的顶点,那我们设置一个father[]数组来记录每个节点的父节点,当然v1的父节点肯定没有,那么我们设置一个结束标志为-1,每次找到一个新的节点就将它的父节点设置为他依附的节点,这样就可以准确的记录边得存储了。

语法:prim(Graph G,int vcount,int father[]);

参数:

G:图,用邻接矩阵表示

vcount:表示图的顶点个数

father[]:用来记录每个节点的父节点

返回值:

null

注意:

常数max_vertexes为图最大节点数

常数infinity为无穷大

数组存储从0开始

如果下面的源程序有错请参照测试程序。

源程序:

 

#define infinity1000000

#definemax_vertexes 5

 

typedef intGraph[max_vertexes][max_vertexes];

 

void prim(GraphG,int vcount,int father[])

{

    int i,j,k;

    int lowcost[max_vertexes];

intcloseset[max_vertexes],used[max_vertexes];

int min;

    for (i=0;i<vcount;i++)

        {

   /* 最短距离初始化为其他节点到1号节点的距离 */

        lowcost[i]=G[0][i];

    /* 标记所有节点的依附点皆为默认的1号节点 */

 

        closeset[i]=0;

        used[i]=0;

        father[i]=-1;

        }

    used[0]=1; /*第一个节点是在U集合里的*/

/* vcount个节点至少需要vcount-1条边构成最小生成树 */

    for (i=1;i<=vcount-1;i++)

        {

        j=0;

       min= infinity;

       /* 找满足条件的最小权值边的节点k */

        for (k=1;k<vcount;k++)

         /* 边权值较小且不在生成树中 */

            if((!used[k])&&(lowcost[k]<min))

         {

              min =  lowcost[k];

              j=k;

           }

        father[j]=closeset[j];

        used[j]=1;;//把第j个顶点并入了U中

        for (k=1;k<vcount;k++)

         /* 发现更小的权值 */

            if(!used[k]&&(G[j][k]<lowcost[k]))

                {

                  lowcost[k]=G[j][k];/*更新最小权值*/

                  closeset[k]=j;/*记录新的依附点*/

                 }

        }

}

测试程序:

测试用例:

1 2 6

1 3 1

1 4 5

2 3 5

2 5 3

3 4 5

3 5 6

3 6 4

5 6 6

4 6 2

#include<stdio.h>

#include<string.h>

#include <stdlib.h>

#define infinity1000000

#definemax_vertexes 6

typedef intGraph[max_vertexes][max_vertexes];

void prim(GraphG,int vcount,int father[])

{   

int i,j,k;

    int lowcost[max_vertexes];

intcloseset[max_vertexes],used[max_vertexes];

int min; 

for(i=0;i<vcount;i++)    

  {

/* 最短距离初始化为其他节点到1号节点的距离 */  

    lowcost[i]=G[0][i];

    /* 标记所有节点的依附点皆为默认的1号节点 */

     closeset[i]=0;     

  used[i]=0;   

    father[i]=-1;     

}   

used[0]=1; /*第一个节点是在s集合里的*/

/* vcount个节点至少需要vcount-1条边构成最小生成树 */ 

  for (i=1;i<=vcount-1;i++)     

   {      

 j=0;

     min = infinity;

       /* 找满足条件的最小权值边的节点k */     

     for (k=1;k<vcount;k++)

         /* 边权值较小且不在生成树中 */    

 if ((!used[k])&&(lowcost[k]<min))

    {

              min =  lowcost[k];

              j=k;

            }      

    father[j]=closeset[j];  

printf("%d%d\n",j+1,closeset[j]+1);//打印边  

used[j]=1;;//把第j个顶点并入了U中    

for(k=1;k<vcount;k++)

         /* 发现更小的权值 */      

   if(!used[k]&&(G[j][k]<lowcost[k]))      

{

                  lowcost[k]=G[j][k];/*更新最小权值*/      

      closeset[k]=j;;/*记录新的依附点*/

    }     

   }

}

                

int main()

{

FILE *fr;

int i,j,weight;

Graph G;

intfatheer[max_vertexes];

for(i=0;i<max_vertexes; i++)

for(j=0;j<max_vertexes; j++)

G[i][j] = infinity;

fr =fopen("prim.txt","r");

if(!fr)

{

printf("fopenfailed\n");

exit(1);

}

while(fscanf(fr,"%d%d%d",&i, &j, &weight) != EOF)

{

G[i-1][j-1] =weight;

G[j-1][i-1] =weight;

}

prim(G,max_vertexes,fatheer);

return 0;

}

程序结果:

3 1

6 3

4 6

2 3

5 2

线段树:lazy_tag