图论--图的最短路径

最短路径

最短路问题指在一张带权图上求解给定源地和汇点之间的最短路径长度,根据给定源点的数量又分为 :
多源最短路:Floyed算法
单源最短路:Dijkstra算法、Bellman-Ford算法、SPFA算法
图论--图的最短路径_第1张图片

多源最短路径算法

Floyed算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法。Floyed 算法在稠密图上效果最佳,边权可正可负。由于三重循环结构紧凑,对于稠密图,效率要高于执行 n次堆优化的 Dijkstra算法。
【算法描述】
图论--图的最短路径_第2张图片
具体看视频讲解

Floyed算法

单源最短路径

1、朴素Dijkstra算法
Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法是很有代表性的最短路径算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等。注意该算法要求图中不存在负权边。
问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径。(单源最短路径)

2.算法描述
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的出边邻接点,则权值为∞。
b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
d.重复步骤b和c直到所有顶点都包含在S中。

Dijkstra

#include 
#define N 21
#define M 99999
int book[N];
int dis[N];
int e[N][N];
int main() {
    int m,n;
    scanf("%d%d",&m,&n);//输入顶点数和边数(有向图)
    for(int i=1; i<=m; i++) {//初始化邻接数组
        for(int j=1; j<=m; j++) {
            if(i==j) {
                e[i][j]=0;
            } else {
                e[i][j]=M;
            }
        }
    }
    book[1]=1;
    int a,b,c;
    for(int i=1; i<=n; i++) {//输入边
        scanf("%d%d%d",&a,&b,&c);
        e[a][b]=c;
    }
    for(int i=1; i<=m; i++) {//初始化距离数组
        dis[i]=e[1][i];
    }
    int u;
    for(int i=1; i<=m-1; i++) {//这个for是控制循环次数的,我要更新距离数组的所有元素,但是第一个点已经不用更新了,因此剩下m-1个点,还需进行m-1次循环
        int mi=M;//每次都假设一个最小值
        for(int j=1; j<=m; j++) {//找出最小值,排序后再找是不现实的,因为排序的过程更改了一些元素的位置
            if(book[j]==0&&dis[j]<mi) {
                mi=dis[j];
                u=j;
            }
        }
        book[u]=1;//找到最小距离的点,表示访问了这个点了,并使用这个点进行中转,这个点的dis值已经是确定值,我们使用这个确定值来尝试到达其他的点,路径就是其他点的出边。
        for(int k=1; k<=m; k++) {//松弛
            if(e[u][k]<M) {//小于M即说明它们之间存在路径,是该点的一条出边,等于则说明不存在,也就没有松弛的必要,
                if(dis[k]>dis[u]+e[u][k]) {
                    dis[k]=dis[u]+e[u][k];//松弛,通过已经确定的点中转
                }
            }
        }
    }
    for(int i=1; i<=m; i++) {
        printf("%d ",dis[i]);
    }
    printf("\n");
    return 0;
}

2、堆优化的Dijkstra算法
后续
3、Bellman-Ford算法
简称Ford(福特)算法,同样是用来计算从一个点到其他所有点的最短路径的算法,也是一种单源最短路径算法。
能够处理存在负边权的情况,但无法处理存在负权回路的情况
算法时间复杂度:O(NE),N是顶点数,E是边数。
算法实现:
设s为起点,dis[v]即为s到v的最短距离,pre[v]为v前驱。w[j]是边j的长度,且j连接u、v。
初始化:dis[s]=0,dis[v]=∞(v≠s),pre[s]=0
For (i = 1; i <= n-1; i++)
For (j = 1; j <= E; j++) //注意要枚举所有边,不能枚举点。
if (dis[u]+w[j] {
dis[v] =dis[u] + w[j];
pre[v] = u;
}

Bell-Ford算法

#include
using namespace std;
#define MAX 999999

int main()
{
    int u[101],v[101],w[101],dis[10];
    int s,n,m;
    int k,i;
    cout << "please input the number of the  city: ";
    cin >> n;
    cout << "is there how many roads: ";
    cin >> m;
    cout << "please input the start: ";
    cin >> s;
    cout << "please input the information of the roads: ";
    for(k=1;k<=m;++k)
    {
        cin >> u[k] >> v[k] >> w[k];
    }
    for(i=1;i<=n;++i)
        dis[i]=MAX;
    dis[s]=0;
    for(k=1;k<=n-1;++k)    //进行n-1轮松弛
    {
        for(i=1;i<=m;++i)   //枚举每一条边
        {
            if(dis[v[i]]>(dis[u[i]]+w[i]))    //对每一条边进行松弛
                dis[v[i]]=dis[u[i]]+w[i];
                //dis[1][v[i]] = dis[u[i]] + w[i];
        }
    }
    for(i=1;i<=n;++i)
        cout << dis[i] << " ";
    return 0;
}

4、SPFA算法
SPFA是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。

主要思想是:
初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束。

这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

SPFA 在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去。

算法时间复杂度:O(kE),E是边数。K是常数,平均值为2。

SPFA

#include
using namespace std;
#define INF 999999

int main ()
{
    int dis[6],que[101]={0},book[6];
    int first[10],next[10];//邻接表存储图 
    int u[8],v[8],w[8];//对应的顶点u,v及边权值w,根据实际情况设置 
    int head=1,tail=1;
    int s,n,m,i;
    cout << "please input the number of the  city: ";
    cin >> n;
    cout << "is there how many roads: ";
    cin >> m;
    cout << "please input the start: ";
    cin >> s;
    cout << "please input the information of the roads: ";
    for(i=1;i<=n;++i)//初始化dis[] 
        dis[i]=INF;
    dis[s]=0;
    for(i=1;i<=n;++i)//标记数组是否入队,0表示未入队 
        book[i]=0;
    for(i=1;i<=n;++i)//表示n个顶点暂时没有边 
        first[i]=-1;
    for(i=1;i<=m;++i)//邻接表 
    {
        cin >> u[i] >> v[i] >> w[i];//读入边 
        next[i]=first[u[i]];//建立邻接表核心语句 
        first[u[i]]=i;
    }
    que[tail]=s;
    ++tail;
    book[s]=1;
    while(head < tail)//该算法核心 
    {
        int k;
        k=first[que[head]];
        while(k != -1)//扫描当前顶点的所有边 
        {
            if(dis[v[k]]>(dis[u[k]]+w[k]))//判断是否松弛 
            {
                dis[v[k]]=dis[u[k]]+w[k];
                if(book[v[k]] == 0)
                {
                    que[tail]=v[k];//下面两条语句入队操作 
                    ++tail;
                    book[v[k]]=1;
                }
            }
            k=next[k];
        }
        book[que[head]]=0;//出队 
        ++head;
    }
    for(i=1;i<=n;i++)//输出 1号顶点到其余顶点的距离 
        cout << dis[i] << " ";
    return 0;
}

例题:香甜的黄油

【参考程序】
#include
#include
#include
using namespace std;
int n,p,c,i,j,x,y,t,min1,head,tail,tot,u;
int a[801][801],b[501],dis[801],num[801],w[801][801],team[1601];
bool exist[801];
int main()
{
    
    cin>>n>>p>>c;
    for(i=1;i<=p;i++)
    {
      b[i]=0;
      num[i]=0;
      for(j=1;j<=p;j++)
        w[i][j]=0x7fffffff/3;
    }
    for(i=1;i<=n;i++)
      cin>>b[i];
    for(i=1;i<=c;i++)                      //邻接矩阵存储
     {
         cin>>x>>y>>t;
         w[x][y]=t;
         a[x][++num[x]]=y;
         a[y][++num[y]]=x;
         w[y][x]=w[x][y];
     }
     min1=0x7fffffff/3;
    for(i=1;i<=p;i++)
     {
       for(j=1;j<=p;j++) dis[j]=0x7fffffff/3;
       memset(team,0,sizeof(team));                         //队列数组初始化
       memset(exist,false,sizeof(exist));                   //exist标志初始化
       dis[i]=0;team[1]=i;head=0;tail=1;exist[i]=true;      //起始点入队
     do {
          head++;
          head=((head-1)%1601)+1;             //循环队列处理
          u=team[head];
          exist[u]=false;
          for(j=1;j<=num[u];j++)
            if (dis[a[u][j]]>dis[u]+w[u][a[u][j]])
            {
              dis[a[u][j]]=dis[u]+w[u][a[u][j]];
              if (!exist[a[u][j]])
               {
                 tail++;
                 tail=((tail-1)%1601)+1;
                 team[tail]=a[u][j];
                 exist[a[u][j]]=true;
               }
            }
       }while(head!=tail);
       tot=0;
       for(j=1;j<=n;j++)
       tot+=dis[b[j]];
       if (tot<min1) min1=tot;
     }
     cout<<min1;
    return 0;
}
  

最短路径相关问题的小技巧

1、某些边只能走有限的次数,可以将图复制多遍,成为分层图
2、给出若干个关键点,求其他点离这些关键点的距离,可以建立一个虚拟点,并使用长度为0的边将虚拟店和这些关键点连接;
3、查找有向图中,所有结点到某个结点的最短路,可以将图反向建边;
4、注意图中是否存在负环、自环、重边等情况。

练习题目

1、单源最短路径
2、邮递员送信
3、飞行路线
4、危险降临

最短路路径输出问题

后续

你可能感兴趣的:(图论,图论,算法)