关于最短路径算法的理解

来源:互联网 发布:现代软件学院马少红 编辑:程序博客网 时间:2024/05/16 06:52

最短路径的算法主要是解决两个问题:

1.单源最短路

2.任意两点间的最短路

主要有三个算法来解决这两个问题:

1.单源最短路:dijkstra算法,spfa算法

2.任意两点间的最短路:floyd算法

----------------------------------------dijkstra单源最短路算法----------------------------------------------------------------

应用范围:边权非负的有向图和无向图均可

算法思想:

1.因为边权非负,所以任意两条边相加的和一定不小于其中任意一条边,我们设源点为s,那么要求所有点到达s的最短路径,那么我们定义一个点集S,定义一个数组dis[],表示某个点只与点集中的点连接的情况到达源点的最短距离,如果暂时不能到达,记为INF,初始情况下点集中只有源点s,那么与源点直接相连的点中边权最小的点一定是最短路径,设最小边连接的点为v,u为除s和v外的任意一点,因为dis[v] < dis[u]且边权非负,所以dis[v]<dis[u]+一条或多条边的边权,所以当前的决策一定是最短路,且之后一定不能更新出更短的路径。

2.那么点v的最短路径已经得到,我们可以把它添加到S点集,然后还要继续维护dis[]数组的性质,也就是如果当前的v点与S点集外的一点u相连,如果dis[v]+edge[v,u] < dis[u],那么点u可以dis[u]就应该更新,因为定义中是定义的经过点集中的点得到的最短的路径,然后这个S点集我们就可以缩点为一个点了,然后又变为了1的情况,知道处理的所有点都计入S点集,也就是整个图缩成了一个点,那么整个图到达s点的最短路径就处理出来了

下面给出O(n^2)的实现方法

邻接矩阵的实现方法:

void dijkstra(int s){       memset(vis , 0 , sizeof(vis));       memset(father , 0 , sizeof(father));       /*初始化dis数组*/       for(int i = 1 ; i<= n ; i++)            dis[i] = INF;       dis[s] = 0;       for(int i = 1 ; i <= n ; i++){/*枚举n个顶点*/          int pos;          pos = -1;          for(int j = 1 ; j <= n ;j++){/*找到未加入集合的最短路点*/             if(!vis[j] && (pos == -1 || dis[j] < dis[pos]))                pos = j;          }          vis[pos] = 1;/*把这个点加入最短路径集合*/          for(int j = 1 ; j <= n ; j++){/*更新dis数组*/             if(!vis[j] && (dis[j] > dis[pos] + value[pos][j])){               dis[j] = dis[pos] + value[pos][j];               father[j] = pos;             }          }       }  }  


主要是两个操作:

1.找出当前S集外的dis[v]最小的点v(可以利用堆优化)

2.将v加入点集S,然后更新dis数组

下面是利用堆优化的O(n*loge)

struct cmp  {      bool operator ( ) ( const int&a , const int&b ) const       {          return dis[a] > dis[b];      }  };    void dijkstra ( )  {      memset ( dis , 0x3f , sizeof ( dis ) );      memset ( used , 0 , sizeof ( used ) );      priority_queue<int , vector<int> , cmp > q;      dis[s] = 0;      q.push ( s );      while ( !q.empty( ) )      {          int u = q.top();          q.pop();          used[u] = true;          for ( int v = 1 ; v <= n ; v++ )              if ( !used[v]&&dis[v] > dis[u] + mp[u][v] )              {                  dis[v] = dis[u] + mp[u][v];                  q.push ( v );              }      }  }  

---------------------------------------------spfa求单源最短路-------------------------------------------------------

适用范围:有负权的图,可以判断负环,如果某个点松弛次数大于等于n,那么说明有负环

算法思想:很直观的宽搜的思想,近似于暴力,复杂度O(ke),k是每个点平均入队次数,可以证明出K<=2

dis数组,记录到源点的最短路径,如果当前不是最短,那么一定能够被某个点点松弛,相反,如果当前点不能够被松弛,那么它就一定是最短路径了

首先科普一个操作:就是松弛操作,这个操作就是利用当前的点的的dis值取更新它能够到达的点的dis值

在宽搜的过程中我们维护一个队列,这个队列中存放的刚刚被松弛过路径的点,初始的时候将源点放入队列中,因为当一个点松弛过后,它的最短路径变短,可能会导致其他点能够被松弛,所以当前点入队,当队列为空也就是没有点能够松弛其他点了,也就是说一个点最多直接间接地被其他n-1个点松弛一遍,所以当松弛数大于等于n的时候证明出现了负环,因为负环能够不断的松弛一条最短路径

代码如下:

struct Edge  {      int v,w,next;  }e[MAX*MAX];    int head[MAX];  int cc;    void add ( int u , int v , int w )  {      e[cc].v = v;      e[cc].w = w;      e[cc].next = head[u];      head[u] = cc++;  }    bool used[MAX];  int dis[MAX];  int cnt[MAX];    bool spfa ( int s , int d )  {      memset ( used , false , sizeof ( used ) );      memset ( dis , -1 , sizeof ( dis ) );      memset ( cnt , 0 ,sizeof ( cnt ) );      queue<int> q;      q.push ( s );      cnt[s]++;      used[s] = true;      dis[s] = 0;      while ( !q.empty())      {          int u = q.front();          q.pop ( );          used[u] = false;          for ( int i = head[u] ; ~i ; i = e[i].next )          {              int v = e[i].v;              if ( dis[v] != -1 && dis[v] <= dis[u] + e[i].w ) continue;              dis[v] = dis[u] + e[i].w;              if ( used[v] ) continue;              used[v] = true;              q.push ( v );              if ( ++cnt[v] > n ) return false;          }      }      return true;  }  
---------------------------------------------floyd求任意两点间的最短路-----------------------------------------------

适用范围:存在负权的图不能用

算法思想:n^3的复杂度不算优秀,但是容易实现。

首先floyd是一个传递闭包的思想,比如A,B,C三个点,A和B存在边,B和C存在边,那么A和C也可以推出直接相连的边

我们枚举这个n个点,然后对于每个点,枚举任意两点,然后更新dis[i][j],就相当于传递闭包当中利用两条边去推导出第三条边,但是floyd求最短时,每两点之间只保留一条边,最小那条,也就是如果本身存在边,那么新添的边与其进行比较,大的那条边被覆盖。整个floyd过程下来相当于整个传递闭包求了一遍,然后所有的边保留下来均是在每次覆盖操作后留下来的最小的,可能有的人会问?如果当前利用k点更新了dis[i][j] = dis[i][k] + dis[k][j],之后又出现了一个v更新了dis[i][k],还是最短的吗?其实是这样的,因为k在更新dis[i][j]的同时一定也会更新dis[v][j],那么到枚举v的时候dis[i][v]和dis[v][j]更新dis[i][j],就相当于用更小的dis[i][k]进行了松弛。

代码如下:

 for ( int k = 0 ; k < n ; k++ )            for ( int i = 0 ; i < n ; i++ )                for  ( int j = 0 ; j < n ; j++ )                        dis[i][j] = max ( dis[i][j] , dis[i][k] + dis[k][j] );


0 0