第三章 搜索与图论(二)(最短路)

时间:2024-03-02 10:08:40

一、最短路问题

1、对于稠密图,由于朴素版的dijkstra算法与边数无关使用这种算法的复杂度较低。稀疏图用堆优化版的算法;单源最短路中存在负权边用SPFA 算法通常较好;多源用floyd算法;

 难点:如何建图,抽象为最短路问题。

二、朴素版dijkstra算法

由于稠密图用这种算法,邻接矩阵存图,注意把g初始化为0x3f;st保存每个数组的状态,

#include<bits/stdc++.h>
//849dijkstra最短路
using namespace std;
const int N=510;
int g[N][N],disk[N],st[N];
int n,m;
int dijkstra()
{
   disk[1]=0;
   for(int i=1;i<=n;i++)
   {
       int t=-1;//找最小的那个
       for(int j=1;j<=n;j++)
          if(st[j]==-1&&(disk[t]>disk[j]||t==-1))t=j;
       st[t]=1;//标记
       for(int j=1;j<=n;j++)
          disk[j]=min(disk[j],disk[t]+g[t][j]);



   }
   if(disk[n]==0x3f3f3f3f)return -1;
   return disk[n];
}
int main()
{
    cin>>n>>m;
    memset(disk,0x3f,sizeof disk);//初始化最短路为正无穷
    memset(g,0x3f,sizeof g);
    memset(st,-1,sizeof st);
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        //由于存在重复带权边,所以要留住那个较短的边因此初始化g为无穷
        g[a][b]=min(g[a][b],c);
    }
    int t=dijkstra();
    cout<<t<<endl;
    return 0;
}

三、堆优化版的dijkstra算法

在获取disk最小的节点的位置进行优化。用堆来存储起点到目标节点的距离。又因为算法每次更新边,更新一次堆的时间复杂度为log(n),时间复杂为O(m log(n))

直接使用优先队列实现堆,优先队列中的元素为pair元素,first为路径长度,second为端点名字。

#include<bits/stdc++.h>
//850dijkstra最短路(堆优化)邻接表存图
using namespace std;
const int N=1e5+10;
typedef pair<int,int> PII;
int h[N],e[N],ne[N],idx,w[N];
int disk[N],st[N];
int n,m;
void add(int a,int b,int c)//将边和权重保存到邻接表中
{
    e[idx]=b;ne[idx]=h[a];
    w[idx]=c,h[a]=idx++;
}
int dijkstra()
{
   disk[1]=0;
   priority_queue<PII,vector<PII>,greater<PII>>heap;//小顶堆
   heap.push({0,1});
   while(heap.size())
   {
       auto t=heap.top();//拿出堆顶
       heap.pop();
       int ver=t.second,distance=t.first;
       if(st[ver]==1) continue;//已经确定过最短路继续
       st[ver]=1;
       for(int i=h[ver];i!=-1;i=ne[i])//遍历与其相连的所有边
       {
           int j=e[i];
           if(disk[j]>distance+w[i])
           {
           //更新后的disk可能不是最小值
           //这些没有用的中间值即使跑到堆顶取出来了
           //但是只要发现对应的点已经确定最短路了就放弃
               disk[j]=distance+w[i];
               heap.push({disk[j],j});
           }
       }
   }


   if(disk[n]==0x3f3f3f3f)return -1;
   return disk[n];
}
int main()
{
    cin>>n>>m;
    memset(disk,0x3f,sizeof disk);//初始化最短路为正无穷
    memset(st,-1,sizeof st);
    memset(h,-1,sizeof h);
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }
    int t=dijkstra();
    cout<<t<<endl;
    return 0;
}

三、  Bellman_Ford算法(用于求有边数限制的最短路)

可以用于用来找负环,或者有最多经过k条边的限制的题目。

而且即使有负环让你求最短路,有k条边的限制也不会出现负无穷的结果

算法:进行指定次数的循环,每次循环都遍历所有边

#include<bits/stdc++.h>
// 853. 有边数限制的最短路 (bellman_ford)
using namespace std;
const int N=510,M=10010;
int n,m,k;
int dist[N],backup[N];
struct Edege//结构体保存边,便于遍历
{
    int a,b,c;
}edges[M];

int bellman_ford()
{
    memset(dist,0x3f,sizeof dist);//初始化
    dist[1]=0;
    for(int i=0;i<k;i++)
    {
        memcpy(backup,dist,sizeof dist);//复制上一次的结果到backup,
        //防止发生串性地更新
        for(int i=0;i<m;i++)
        {
            int a=edges[i].a,b=edges[i].b,w=edges[i].c;
            dist[b]=min(dist[b],backup[a]+w);
        }
    }
    //由于存在负边,可能会小于0x3f3f3f3f,根据题目的数据范围,计算最多减的次数
    if(dist[n]>=0x3f3f3f3f/2)return -1;
    else return dist[n];
}
int main()
{
    cin>>n>>m>>k;
    for(int i=0;i<m;i++)
    {
       int a,b,w;
       cin>>a>>b>>w;
       edges[i]={a,b,w};//保存边
    }
    int t=bellman_ford();
    if(t==-1)puts("impossible");
    else cout<<t<<endl;
    return 0;

}

四、SPFA算法求最短路

SPFA算法其实就是在Bellman-ford算法基础上的优化。Bellman-ford算法看起来比较傻,每次迭代的话是遍历所有边来更新,但是每次迭代的话不是每条边都会更新。SPFA算法就是对这个做优化,每次迭代dist[b]可以更新的话,一定是dist[a]变小了,因为如果dist[a]不变的话,dist[b]一定不变。只有dist[a]变小了,它的后继才会变小。所以SPFA算法就从这个点进行优化。

SPFA算法的思路就是迭代的时候用一个队列来做,队列里面存的就是到起点距离变小的点。先把起点放到队列里面去,只要队列不空,也就是队列里面还有距离变小的点的话,就执行一下操作:

先取出队头t,然后队头出队
更新t的所有出边,t到起点的距离不是变小了吗, 那么所有和t相连的点都有可能变小,如果更新成功的话,就入队。但是注意要判断一下这个点已经入过队的话就不用重复加入了。

————————————————           
原文链接:https://blog.csdn.net/qq_52905520/article/details/126574584

例题:利用spfa算法求最短路,从队列头拿出结点,只要能更新就更新,并把更新的点加入到队列中,并利用st bool数组保存队列中是否已经有当前结点。

spfa算法中不使用backup的原因是每次更新只更新邻边,不会出现串联更新。

#include<bits/stdc++.h>
//
using namespace std;
const int N=1e5+10;
int h[N],e[N],ne[N],idx,w[N];
int dist[N];
bool st[N];
int n,m;
void add(int a,int b,int c)
{
    e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;

}
int spfa()
{
    memset(dist,0x3f,sizeof dist);
    memset(st,false,sizeof st);
    dist[1]=0;
    queue<int>q;//队列存所有更新过的点
    dist[1]=0;
    q.push(1);//入队
    while(q.size())
    {
        int t=q.front();//拿出一个元素
        q.pop();
        st[t]=false;
        for(int i=h[t];i!=-1;i=ne[i])
        {
          int j=e[i];
          //首先更新
          if(dist[j]>dist[t]+w[i])
          {
            dist[j]=dist[t]+w[i];//先更新
            if(!st[j])//只有队列里面没有的时候才能入队
            {
                q.push(j);
                st[j]=true;
            }
          }
        }
    }
    if(dist[n]==0x3f3f3f3f)return -1;
    return dist[n];
}
int main()
{
  cin>>n>>m;
  memset(h,-1,sizeof h);
  while(m--)
  {
      int a,b,c;
      cin>>a>>b>>c;
      add(a,b,c);
  }
  int t=spfa();
  if(t==-1)puts("impossible");
  else cout<<t<<endl;
  return 0;
}

五、spfa算法判断是否有负环

通过最短路的边数来判断是否有负环 。

#include<bits/stdc++.h>
//852 spfa判断是否
using namespace std;
const int N=1e5+10;
int h[N],e[N],ne[N],idx,w[N];
int dist[N];
bool st[N];
int cnt[N];//记录边数
int n,m;
void add(int a,int b,int c)
{
    e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;

}
int spfa()
{
    memset(dist,0x3f,sizeof dist);
    memset(st,false,sizeof st);
    dist[1]=0;
    queue<int>q;//队列存所有更新过的点
    //因为有可能负环从第一个结点到不了所以把所有结点放到队列
    for(int i=1;i<=n;i++)q.push(i),st[i]=true;
    dist[1]=0;

    while(q.size())
    {
        int t=q.front();//拿出一个元素
        q.pop();
        st[t]=false;
        for(int i=h[t];i!=-1;i=ne[i])
        {
          int j=e[i];
          //首先更新
          if(dist[j]>dist[t]+w[i])
          {
            dist[j]=dist[t]+w[i];//先更新
            cnt[j]=cnt[t]+1;
            if(cnt[j]>n)return 1;
            if(!st[j])//只有队列里面没有的时候才能入队
            {
                q.push(j);
                st[j]=true;
            }
          }
        }
    }
    return 0;
}
int main()
{
  cin>>n>>m;
  memset(h,-1,sizeof h);
  while(m--)
  {
      int a,b,c;
      cin>>a>>b>>c;
      add(a,b,c);
  }
  int t=spfa();
  if(spfa())puts("Yes");
  else puts("No");
}

六、弗洛伊德Floyd算法求多源最短路

三重循环

#include<bits/stdc++.h>
//852 spfa判断是否
using namespace std;
const int N=210,INF=1e9;
int d[N][N];
int n,m,Q;
void floyd()
{

    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
               d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
int main()
{
    cin>>n>>m>>Q;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(i==j)d[i][j]=0;
            else d[i][j]=INF;
        }
    }
    while(m--)
    {
       int a,b,c;
       cin>>a>>b>>c;
       d[a][b]=min(d[a][b],c);
    }
    floyd();
    while(Q--)
    {
        int a,b;
        cin>>a>>b;
        if(d[a][b]<INF/2) cout<<d[a][b]<<endl;
        else cout<<"imposible"<<endl;
    }

}