最短路径

Dijkstra算法

适用条件:

1.单源路径

2.有向图或无向图

3.无负边权


1. Dijkstra算法思想

设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组。

第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将 加入到集合S中,直到全部顶点都加入到S中,算法就结束了)。

第二组为其余未确定最短路径的顶点集合(用U表示)。

按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度。U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。


2. Dijkstra算法具体步骤  

(1)初始时,S只包含源点,即S,v的距离为0。U包含除v外的其他顶点,U中顶点u距离为边上的权(若v与u有    边)。

(2)从U中选取一个距离v最小的顶点k,把k加入S中(该选定的距离就是v到k的最短路径长度)。

(3)以k为新考虑的中间点,修改U中各顶点的距离。若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值为顶点k的距离加上边上的权值。

(4)重复步骤(2)和(3)直到所有顶点都包含在S中。


3. Dijkstra算法举例说明

如下图,设A为源点,求A到其他各顶点(B、C、D、E、F)的最短路径。线上所标注为相邻线段之间的距离,即权值。

图一:Dijkstra无向图


 

算法执行步骤如下表:



4.Dijkstra算法实现

#include <iostream>    
#define MAX 100    
#define INF 1000000000    
using namespace std; 

int map[MAX][MAX];

int Dijkstra (int map[][MAX],int n, int s,int e)  //n个节点,s为起点,e为终点  
{    
	int dis[MAX];    
	int mark[MAX];   //记录被选中的结点     
	int i,j,k = 0;    
	for(i = 1 ; i <= n ; i++)   //初始化所有结点,每个结点都没有被选中     
		mark[i] = 0;    
	for(i = 1 ; i <= n ; i++)   //将每个结点到start结点weight记录为当前distance      
        dis[i] = map[s][i];    
	
    mark[s] = 1;         //start结点被选中            
    dis[s] = 0;         //将start结点的的距离设置为0     
    int min ;          //设置最短的距离。     
    for(i = 1 ; i < n; i++)    
    {    
        min = INF;    
        for(j = 1; j <=n;j++)    
        {    
            if(mark[j] == 0  && dis[j] < min)    //未被选中的结点中,距离最短的被选中     
            {    
                min = dis[j] ;    
                k = j;    
            }    
        }    
        mark[k] = 1;      //标记为被选中     
        for(j = 1 ; j <= n ; j++)    
        {    
            if( mark[j] == 0  && dis[j] > (dis[k] + map[k][j]))  //修改剩余结点的最短距离         
                dis[j] = dis[k] + map[k][j];       
        }    
    }    
    return dis[e];        
} 

int main()    
{    
    int n,m,a,b,dis;
    int i,j;    
    while(scanf("%d %d",&n,&m))    
    {       
        if(n == 0 || m == 0)    break;    
		
        for(i=1;i<=n;i++)    
            for(j =1;j<= n;j++)    
                map[i][j] = INF; 
			
			for(i = 0 ; i < m ;i++)    
			{    
				scanf("%d %d %d",&a,&b,&dis);       
				if(dis < map[a][b] )    
					map[a][b] = map[b][a] = dis;    
			}  
			
			int ans = Dijkstra(map,n,1,n);    
			printf("%d\n",ans);    
    }    
	return 0;
}  




Bellman-ford算法

适用条件:

1.单源路径

2.有向图或无向图

3.边权可正可负


1.Bellman-Ford算法思想
对于给定的带权(有向或无向)图 G=(V,E),其源点为s,加权函数 w是 边集 E 的映射。

对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。
 
2.Bellman-Ford算法流程:
(1)初始化:将除源点外的所有顶点的最短距离估计值 d[v] ← +∞, d[s] ←0。
(2)迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最          短距离。(运行v-1次)
(3)检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明          问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在 d[v]中。

描述性证明:
1).图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。

2).从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。

3).在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1 条边,所以,只需要循环|v|-1 次。

4).每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?)


5).如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。

3.Bellman-Ford算法实现

#include<iostream>    
#include<cstdio>    
using namespace std;    
#define MAX 0x3f3f3f3f    
#define N 1010   

int n, m, l; //点,边,起点   
int dis[N], pre[N]; 

struct Edge //边    
{    
    int u, v;    
    int cost;    
}edge[N];        

int Bellman_Ford()    
{    
    int i,j;
	for( i = 1; i <= n; i++)   //初始化    
        dis[i] = (i == l ? 0 : MAX);
    
    for( i = 1; i <= n - 1; i++)    
        for( j = 1; j <= m; j++)    
            if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost)   //松弛(顺序一定不能反)    
            {    
                dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;    
                pre[edge[j].v] = edge[j].u;    
            }    
            
	    int flag = 1;        //判断是否含有负权回路    
            for( i = 1; i <= m; i++)    
                if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)    
                {    
                    flag = 0;    
                    break;    
                }    
                return flag;    
}    

void print_path(int root)            //打印最短路的路径(反向)    
{    
    while(root != pre[root])    //前驱    
    {    
        printf("%d-->", root);    
        root = pre[root];    
    }    
    if(root == pre[root])    
        printf("%d\n", root);    
}    

int main()    
{    
    scanf("%d%d%d", &n, &m, &l);    
    pre[l] = l;    
    for(int i = 1; i <= m; ++i)      
        scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);    
	
    if(Bellman_Ford())    
        for(int i = 1; i <= n; ++i)   //每个点最短路    
        {    
            printf("%d\n", dis[i]);    
            printf("Path:");    
            print_path(i);    
        }    
	else    
		printf("have negative circle\n");    
		return 0;    
} 




Floyd-Warshall算法


Floyd-Warshall算法是解决任意两点间的最短路径的算法。

可以处理有向图或负权值的最短路径问题,同时也被用于计算有向图的传递闭包。

设D_{i,j,k}为从i到j的只以(1..k)集合中的节点为中间节点的最短路径的长度。
若最短路径经过点k,则D_{i,j,k}=D_{i,k,k-1}+D_{k,j,k-1};
若最短路径不经过点k,则D_{i,j,k}=D_{i,j,k-1}。
因此,D_{i,j,k}={min}(D_{i,k,k-1}+D_{k,j,k-1},D_{i,j,k-1})。


设图G中n 个顶点的编号为1到n。

令c [i, j, k]表示从i 到j 的最短路径的长度,其中k 表示该路径中的最大顶点,也就是说c[i,j,k]这条最短路径所通过的中间顶点最大不超过k。

如果G中包含边<i, j>,则c[i, j, 0] =边<i, j> 的长度;若i= j ,则c[i,j,0]=0。

如果G中不包含边<i, j>,则c (i, j, 0)= +∞。c[i, j, n] 则是从i 到j 的最短路径的长度。
对于任意的k>0,通过分析可以得到:
中间顶点不超过k 的i 到j 的最短路径有两种可能:该路径含或不含中间顶点k。

若不含,则该路径长度应为c[i, j, k-1],

否则长度为 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取两者中的最小值。

状态转移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。


核心代码:

for(int k =1 ;  k <= n ; k ++ ){  
    for(int i =1 ; i<= n ; i++){  
        for(int j =1 ;j<=n;j++){  
               dist[ i ][ j ]= min( dist[ i ][ j ],dist[ i ][ k ]+dist[ k ][ j ] );        
          }  
     }  
}  

Floyd算法实现

#include<iostream>
#include<cstdio>
using namespace std;
int map[11111][11111];
#define INF 999999
int main()
{
    int i,j,k;
    int n,m;
    int a,b,c;
    while(cin>>n>>m)
    {
        if(n==0 && m==0) break;
        for(i=1;i<=n;i++)
            for(j=1;j<=n;j++)
            {
                if(i==j)
                map[i][j]=0;
                else
                map[i][j]=INF;
            }
        for(i=1;i<=m;i++)
        {
            cin>>a>>b>>c;
            map[a][b]=map[b][a]=c;
        }

        for(i=1;i<=n;i++)
          for(j=1;j<=n;j++)
            for(k=1;k<=n;k++)
           {
            if(map[j][k]>map[j][i]+map[i][k])
            map[j][k]=map[j][i]+map[i][k];
           }
        cout<<map[1][n]<<endl;
    }
    return 0;
}


SPFA算法

代码实现模板


邻接表:

long long SPFA(int st)  
{  
    for(int i=1;i<=n;i++)  
        sp[i]=inf;  
    sp[1]=0;  
    queue<int> q;  
    q.push(st);  
    while(!q.empty())  
    {  
        int kai=q.front();q.pop();  
        for(int i=H[kai];i!=-1;i=E[i].next)  
        {  
            if(sp[E[i].v]>E[i].count+sp[kai]){  
                sp[E[i].v]=E[i].count+sp[kai];  
                q.push(E[i].v);  
            }  
        }  
    }  
    long long ans=0;  
    for(int i=1;i<=n;i++)  
        ans+=sp[i];  
    return ans;  
}  


邻接矩阵:

void spfa(int s,int dis[])  
{  
    int i,pre[N];  
    bool used[N];  
    queue<int> q;  
    memset(used,0,sizeof(used));  
    memset(pre,-1,sizeof(pre));  
    for(i=0; i<N; i++)  
        dis[i]=inf;  
    dis[s]=0;  
    used[s]=true;  
    q.push(s);  
    while(!q.empty())  
    {  
        int u=q.front();  
        q.pop();  
        used[u]=false;  
        for(i=0; i<map[u].size(); i++)  
        {  
            Node p=map[u][i];  
            if(dis[p.v]>dis[u]+p.len)  
            {  
                dis[p.v]=dis[u]+p.len;  
                pre[p.v]=u;  
                if(!used[p.v])  
                {  
                    used[p.v]=true;  
                    q.push(p.v);  
                }  
            }  
        }  
    }  
}  


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值