最短路问题

1、单源汇最短路(单起点)

<1>边权均为正数

(1)朴素Dijkstra算法 

思路:

①用dis[]数组来维护所有点到起点的最短距离,假设起点为1,则初始化dis[1]=0,其余各点dis[i]='+∞'(大过题目范围即可,一般取INF=0x3f3f3f3f)

②迭代i:1~n(n个点,m条边)

对每轮迭代,将已经确定了最短距离的点打上标记放入集合S中,再找到S外的点离起点最近距离的点t,再用t维护其他点到起点的距离,更新dis[i]

在第k轮迭代中dis[i]的意义表现为从起点经过不超过k条边到达i点的最短距离

迭代完n次后即可得到每个点到起点的最短距离dis[i]

注意事项:

①朴素Dijkstra算法一般用于稠密图,体现为点少边多(相对而言),采用邻接矩阵d[a][b]存储a->b的有向边

②注意初始化各数组

时间复杂度:O(mn)

附上代码:

//单源汇最短路:正权边+稠密图
//朴素Dijkstra算法
//邻接矩阵存有向图
#include<iostream>
#include<cstring>

using namespace std;

const int N=1e3+10,M=1e5+10,INF=0x3f3f3f3f;//存取相关参数,具体情况依题目而定
//N代表点数,M代表边数,INF代表+∞

int d[N][N];//邻接矩阵存有向图
int dis[N];//维护每个点到起点的最短距离
bool st[N];//标记已经确定了最短距离的点集,集合内的点标记为true

int n,m;//点数、边数,这里一律默认起点为1号点,终点为号点,故不再设起点终点,具体实际情况可能不一样,有可能需要再设两个变量存储起点终点


int dijkstra()
{
    //初始化dis[]
    memset(dis,0x3f,sizeof dis);

    //把起点加进来
    dis[1]=0;

    //开启n轮迭代
    for(int i=1;i<=n;i++)
    {
        int t=-1;//t将存储集合外的最近点
        for(int j=1;j<=n;j++)
        {
            if(!st[j]&&(t==-1||dis[t]>dis[j]) t=j;    //集合外+最近点
        }
        
        //把t先加进集合
        st[t]=true;
        
        //然后更新其他点到起点最近点的距离
        for(int j=1;j<=n;j++) dis[j]=min(dis[j]+d[t][j]);
    }
    
    return dis[n];
}

int main()
{
    cin>>n>>m;

    //维护d[][]
    memset(d,0x3f,sizeof d);
    for(int i=1;i<=n;i++) d[i][i]=0;
    while(m--)
    {
        int a,b,w;
        cin>>a>>b>>w;//从a->b长度为w的有向边
        d[a][b]=min(d[a][b],w);    //防止重边和自环
    }
   
    //运行朴素Dijkstra算法
    int t=dijkstra();
    
    if(t!=INF) cout<<t<<endl;
    else cout<<"impossible"<<endl;    //如果返回INF,说明从起点到不了终点

    return 0;
}

上述代码中仍有以下缺陷:

①对于邻接矩阵存储有向边的方法,当点数过于多时会出现存储困难(数组开不了太大)

②每轮迭代采用枚举寻找最近点,得把每条边遍历一遍,不够高效

但朴素Dijkstra算法代码简洁,在处理稠密图时具有相当大的优势,对于其他情形,需要对该算法进行优化

(2)堆优化版Dijkstra算法--->稀疏图(点多边少)

优化途径:

①用邻接表存有向图,可以存更多的点,更加高效

②采用小根堆来存储每个点到起点的距离,堆顶元素即为最近点,直接弹出堆顶元素即可,不用枚举

其余细节均与朴素Dijkstra同

思路:

①用dis[]数组来维护所有点到起点的最短距离,假设起点为1,则初始化dis[1]=0,其余各点dis[i]='+∞'(大过题目范围即可,一般取INF=0x3f3f3f3f)

②迭代i:1~n

对每轮迭代,将已经确定了最短距离的点打上标记放入集合S中,再找到S外的点离起点最近距离的点t,再用t维护其他点到起点的距离,更新dis[i]

在第k轮迭代中dis[i]的意义表现为从起点经过不超过k条边到达i点的最短距离

迭代完n次后即可得到每个点到起点的最短距离dis[i]

注意事项:

①朴素Dijkstra算法一般用于稀疏图,体现为点多边少(相对而言),采用邻接表存储a->b的有向边

②注意初始化各数组

时间复杂度:O(nlogm)

附上代码:

#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>

using namespace std;

typedef pair<int,int> PII;

const int N=2e5+10,M=2e5+10,INF=0x3f3f3f3f;

int h[N],ne[M],e[M],w[M],idx;

int dis[N];
bool st[N];

int n,m;

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int dijkstra()
{
    //初始化dis[]
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    
    //声明小根堆
    priority_queue<PII,vector<PII>,greater<PII>> heap;
    heap.push({0,1});//把起点插入堆,第1个参数表示到起点距离,第2个参数是点的编号
    
    while(heap.size())//当堆中还有元素,迭代继续
    {
        auto t=heap.top();//找到集合外最近点,并弹出
        heap.pop();
        
        int p=t.second,d=t.first;
        if(st[p]) continue;      
        //如果p标记了true,说明之前已经更新过p点了,这条边为重边,肯定比之前那条便长,不用继续更新
        st[p]=true;//标记p点
        
        //用t维护其他点到起点最近距离
        for(int i=h[p];i!=-1;i=ne[i]) //更新所有p点连接的点
        {
            int j=e[i];
            int d1=w[i];
            //p可到达j点,距离为d1
            if(dis[j]>d+d1)
            {
                dis[j]=d+d1;
                heap.push({dis[j],j});//把更新后的距离插入到堆中
            }
        }
    }
    
    return dis[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=dijkstra();
    
    if(t!=INF) cout<<t<<endl;
    else cout<<"-1"<<endl;
    
    return 0;
    
}

<2>存在负权边

(1)Bellman-Ford算法

思路:


①用结构体数组存储每条a->b的边w,初始化dis[1]=0,其余各边dis[i]=‘+∞'

②迭代i:1~n                (对每轮迭代k,dis[i]仍表现为经过不超过k条边到达i点的最短距离)

        枚举每一条边a,b,w,更新dis[b]

               dis[b]=min(dis[b],dis[a]+w)

注意:

上式子代表起点到b有两个方案:①走上一轮迭代留下的dis[b] ②走不超过k-1条边留下的dis[a]+a->b 也就是说dis[a]必须是在上一轮迭代中(k-1)留下的,但实际上在第k轮迭代中,可能dis[a]已经更新到了第k轮迭代,故需要备份 备份 备份!(重要的事情说三遍~)

Bellman-Ford算法其实就是把到达一个点的所有情况都枚举一遍,然后存储所有情况中最小的值

时间复杂度:O(mn)

附上代码:

//Bellman-Ford算法:负权边最短路Ⅰ
#include<iostream>
#include<cstring>

using namespace std;

const int N=510,M=1e5+10,INF=0x3f3f3f3f;

struct Edge
{
    int a,b,w;
}edges[M];              //结构体数组存储边

int dis[N];
int last[N];            //备份上一轮迭代的值
int n,m,k;

int bellman_ford()
{
    //初始化dis[]
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    
    for(int i=1;i<=n;i++)
    {
        memcpy(last,dis,sizeof dis);
        
        //枚举每条边
        for(int j=0;j<m;j++)
        {
            int a=edges[j].a,b=edges[j].b,w=edges[j].w;
            dis[b]=min(dis[b],last[a]+w);
            //last[a]为上一轮留下的dis[a]
        }
    }
    
    return dis[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};
    }
    
    //运行Bellman-Ford算法
    int t=bellman_ford();
    
    if(t>INF/2) cout<<"impossible"<<endl;
    else cout<<t<<endl;
    
    return 0;
    
}

该算法具有以下缺陷:

①为找到最短路,枚举了所有的情况,效率过低

(但由于这种算法可以求出经过不超过k条边的最短路,大多仅使用在有边数限制的负权最短路问题中)

(2)SPFA算法

优化途径:

在Bellman-Ford算法的

                                dis[b]=min(dis[b],dis[a]+w)

显然,只有当第k-1轮迭代dis[a]有更新,在第k轮才有可能更新dis[b]

(证明如下:

在第k-1轮中:

                                         dis[b]_{k-1}=(dis[b]_{k-2},dis[a]_{k-2}+w)

若第k-1轮迭代没更新dis[a],即

                                         dis[a]_{k-1}=dis[a]_{k-2}

所以

dis[b]_{k}=min(dis[b]_{k-1},dis[a]_{k-1}+w)

                                                           =min(dis[b]_{k-1},dis[a]_{k-2}+w)

                                                           =dis[b]_{k-1}

即dis[b]不会再更新)

故SPFA算法中用一个队列来存储每轮迭代中更新过的点,以便于确定下一轮迭代中有可能需要再更新距离的点,而不是枚举所有情况

思路:

①用邻接表存储a->b的有向边w

②用队列来存储每轮迭代中更新过dis[]距离的点,直至队列不再含有元素时代表所有最短距离已经更新完毕

时间复杂度:大部分情况为O(m),最坏情况下为O(mn)

附上代码:

//SPFA算法:负权边最短路Ⅱ
#include<iostream>
#include<cstring>
#include<queue>

using namespace std;

const int N=1e5+10,M=1e5+10,INF=0x3f3f3f3f;

int h[N],e[M],ne[M],w[M],idx;

int dis[N];
int st[N];//标记队列中元素,避免重边时重复入队

int n,m;

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int spfa()
{
    //初始化dis[]
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    
    queue<int>q;
    q.push(1);//起点入队
    
    while(q.size())//当队列中有元素,说明最短路还需要维护
    {
        int t=q.front();//弹出队头
        q.pop();
        st[t]=false;//t已经出队了,消除标记
        
        for(int i=h[t];i!=-1;i=ne[i])
        {
            //更新所有t可以到达的边
            int j=e[i];
            int d=w[i];
            if(dis[j]>dis[t]+d)
            {
                dis[j]=dis[t]+d;
                if(!st[j]) 
                {
                    //当j不在队列里,打上标记并入队
                    st[j]=true;
                    q.push(j);
                }
            }
        }
    }
    
    return dis[n];
    
}

int main()
{
    cin>>n>>m;
    
    //初始化邻接表
    memset(h,-1,sizeof h);
    while(m--)
    {
        int a,b,w;
        cin>>a>>b>>w;
        add(a,b,w);
    }
    
    int t=spfa();
    
    if(t>INF/2) cout<<"impossible"<<endl;
    else cout<<t<<endl;
    
    return 0;
}

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值