单源最短路径floyd、dijkstra、dijkstra+ heap、bellman-flod、spfa

畅通工程续

Problem Description

某省自从实行了很多年的畅通工程计划后,终于修建了很多路。不过路多了也不好,每次要从一个城镇到另一个城镇时,都有许多种道路方案可以选择,而某些方案要比另一些方案行走的距离要短很多。这让行人很困扰。

现在,已知起点和终点,请你计算出要从起点到终点,最短需要行走多少距离。

Input

本题目包含多组数据,请处理到文件结束。
每组数据第一行包含两个正整数N和M(0<N<200,0<M<1000),分别代表现有城镇的数目和已修建的道路的数目。城镇分别以0~N-1编号。
接下来是M行道路信息。每一行有三个整数A,B,X(0<=A,B<N,A!=B,0<X<10000),表示城镇A和城镇B之间有一条长度为X的双向道路。
再接下一行有两个整数S,T(0<=S,T<N),分别代表起点和终点。

Output

对于每组数据,请在一行里输出最短需要行走的距离。如果不存在从S到T的路线,就输出-1.

Sample Input

3 3
0 1 1
0 2 3
1 2 1
0 2
3 1
0 1 1
1 2

Sample Output

2
-1

floyd 暴力算法,核心代码三个for

#include<bits/stdc++.h>

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

int arr[N][N];
int n,m,st,ed;

inline void init()
{
    for(int i=0;i<n;++i)
        for(int j=0;j<n;++j)
            arr[i][j]=(i==j?0:inf);
}

void floyd()//时间复杂度o(n^3) 数据量小于1000时可用
{
    for(int k=0;k<n;++k)
        for(int i=0;i<n;++i)
            for(int j=0;j<n;++j)
                arr[i][j]=min(arr[i][j],arr[i][k]+arr[k][j]);//直接从i到j还是i到中转点k后到j近取最小更新最短

}

int main()
{
    int a,b,c;
    while(cin>>n>>m)
    {
        init();//初始化
        for(int i=0;i<m;++i)
        {
            cin>>a>>b>>c;
            if(arr[a][b]>c)arr[a][b]=arr[b][a]=c;//因为是双向,所以尽量取最小
        }

        floyd();

        cin>>st>>ed;
        cout<<(arr[st][ed]==inf?-1:arr[st][ed])<<endl;

    }
    return 0;
}


dijsktra算法

用于无负边权的 单元 最短路径问题。

#include<bits/stdc++.h>

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

int arr[N][N];
int dis[N];//存储某个点到各个点的最短路径
bool vis[N];//不能成环,所以判断某个点是否走过
int n,m,st,ed;

inline void init()
{
    for(int i=0;i<n;++i)
        for(int j=0;j<n;++j)
            arr[i][j]=(i==j?0:inf);
}

void dijkstra()//每次循环找最短边,然后更新某个点通过最短边到每个点的最短路径,每更新一次标记用过的边防止形成环
{
    int k=0;
    for(int i=0;i<n-1;++i)//某个点到其他点的最短路肯定不是环,所以n个点最多有n-1条边,要找的最短路径可能走前面的最短路径,所以最多循环n-1次一定可以找到
    {
        int minv=inf;//循环每次为了找到某个点到其他点的最短边
        for(int j=0;j<n;++j)
            if(dis[j]<minv&&!vis[j])minv=dis[j],k=j;//第一次这里忘了加vis错误,因为不能走走过的点
        vis[k]=true;//遍历过后找到了一个最短的边
        for(int v=0;v<n;++v)
        {
            if(arr[k][v]<inf)
            {
                dis[v]=min(dis[v],dis[k]+arr[k][v]);
            }
        }
    }

}

int main()
{
    int a,b,c;
    while(cin>>n>>m)
    {
        init();
        for(int i=0;i<m;++i)
        {
            cin>>a>>b>>c;
            if(arr[a][b]>c)arr[a][b]=arr[b][a]=c;
        }

        cin>>st>>ed;

        for(int i=0;i<n;++i)dis[i]=arr[st][i];//初始化起点
        memset(vis,false,sizeof vis);//初始化用过的点
        vis[st]=true;

        dijkstra();
        cout<<(dis[ed]==inf?-1:dis[ed])<<endl;
    }

    return 0;
}


dijkstra+ heap算法

时间复杂度o((m+n)logn)级别小于n^2。利用优先队列自动排序,寻找最短边。这里有两种写法,一种用二维数组存储,另一种用邻接表存储。邻接表适用于边特别多的情况。

#include<bits/stdc++.h>

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

typedef pair<int,int>PII;

int arr[N][N];
int dis[N];
bool vis[N];
int n,m,st,ed;

inline void init()
{
    memset(dis,inf,sizeof dis);
    memset(vis,false,sizeof vis);

    for(int i=0;i<n;++i)
        for(int j=0;j<n;++j)
            arr[i][j]=(i==j?0:inf);
}

void dijkstra()
{
    dis[st]=0;//忘记了,wa,从st开始,dis[st]=0,否则在例如第一次循环的过程中,到其他点距离为0x3f3f3f3f+某个值不越界,但是很大,第一次相当于答案加了0x3f3f3f3f
    priority_queue<PII,vector<PII>,greater<PII> > q;
    q.push({0,st});

    while(q.size())
    {
        PII now=q.top();q.pop();//greater小顶堆自动排序队首为最小,less队首最大大顶堆。用这个来找最短边。
        int vi=now.second;//最短边的另一端端点
        if(vis[vi])continue;
        vis[vi]=true;
        for(int i=0;i<n;++i)
        {
            if(!vis[i]&&dis[i]>dis[vi]+arr[vi][i])//比较通过最短边是否为最短路径,更新到每个点的最短路径
            {
                dis[i]=dis[vi]+arr[vi][i];
                q.push({dis[i],i});
            }
        }
    }
}

int main()
{
    int a,b,c;
    while(cin>>n>>m)
    {
        init();//初始化应该在输入之前,否则数据没输入。
        for(int i=0;i<m;++i)//这里不要错,m个店
        {
            cin>>a>>b>>c;
            arr[a][b]=arr[b][a]=min(arr[a][b],c);
        }
        cin>>st>>ed;

        dijkstra();
        cout<<(dis[ed]==inf?-1:dis[ed])<<endl;

    }
    return 0;
}

#include<bits/stdc++.h>

//邻接表写法。存在大量边的情况下适合用邻接表

#define PUSH(x,y,z) G[x].push_back({y,z});

using namespace std;

const int inf=0x3f3f3f3f;
const int N=205;

typedef pair<int,int> P;

int n,m,st,ed;
vector<P> G[N];

int dis[N];
bool vis[N];

inline void init()
{
    memset(dis,inf,sizeof dis);
    memset(vis,false,sizeof vis);

    for(int i=0;i<n;++i)G[i].clear();
}

void dijkstra()
{
    dis[st]=0;
    priority_queue<P,vector<P>,greater<P> > q;
    q.push({0,st});
    while(q.size())
    {
        P now=q.top();
        q.pop();//选最短边,找到最短边的另一个端点
        int u=now.second;
        if(vis[u])continue;
        vis[u]=true;
        for(int i=0;i<G[u].size();++i)
        {
            int v=G[u][i].first;
            int cost=G[u][i].second;
            if(!vis[v]&&dis[v]>dis[u]+cost)//遍历更新,通过最短边到点G[u][i].first的最短距离。检测vis[v]是否走过,因为要更新通过u到各个点,防止成环。
            {
                dis[v]=dis[u]+cost;
                q.push({dis[v],v});//仅c++11以上版本编译器支持。建议写成make_pair(int,int),make_pair作用是把参数变成pair类型,兼容所有版本,写法:q.push(make_pair<int,int>);
            }
        }
    }
}

int main()
{
    int a,b,c;
    while(cin>>n>>m)
    {
        init();
        for(int i=0;i<m;++i)
        {
            cin>>a>>b>>c;
            PUSH(a,b,c);//不用判断G[a][b].second>c因为,首先第一次输入用不上,其次如果出现重复数据,在更新的时候不影响更新他的最小值。
            PUSH(b,a,c);
        }

        cin>>st>>ed;

        dijkstra();

        /*for(int i=0;i<m;++i)
        {
            for(int j=0;j<G[i].size();++j)
            {
                cout<<G[i][j].first<<" "<<G[i][j].second<<endl;
            }
        }*/

        cout<<(dis[ed]==inf?-1:dis[ed])<<endl;
    }

    return 0;
}

bellman-flod算法

dijkstra无法解决负权问题,此算法可以,核心代码4行,循环n - 1次。

如果存在负边权,则无最短路径。

如何保证不成环,因为总共对每条边最多做n - 1次松弛,每次松弛只有两种可能情况(1)利用未用过的较短的边松弛(2)利用负边松弛,且只要用负边权,下次还会用用过的负边权。所以松弛n - 1次,最多利用n - 1条边,总共n个点所以不会成环。

#include<bits/stdc++.h>

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e3 + 5;

int n,m,st,ed;
int pro[N],dis[N],u[N],v[N],w[N];

void Bellman_Ford()
{
    for( int k = 0;k < n - 1; ++ k )
    {
        for( int i = 0;i < n; ++ i )pro[i] = dis[i];
        for( int i = 0;i < m; ++ i )
        {
            dis[v[i]] = min(dis[v[i]],dis[u[i]] + w[i]);
            dis[u[i]] = min(dis[u[i]],dis[v[i]] + w[i]);//无向图需要反向更新一次
        }

        bool check = false;
        for( int i = 0;i < n; ++ i )
        {
            if(pro[i] != dis[i])
            {
                check = true;
                break;
            }
        }
        if(check)continue;
        else break;
    }

    bool check = false;
    for( int i = 0;i < m; ++ i )
    {
        if(dis[v[i]] > dis[u[i]] + w[i])
        {
            check = true;
            break;
        }
    }

    if(check)cout << "-1" << endl;
}

int main()
{
    while(cin >> n >> m)
    {
        for( int i = 0;i < m; ++ i )cin >> u[i] >> v[i] >> w[i];
        memset(dis,INF,sizeof dis);

        cin >> st >> ed;

        dis[st] = 0;

        Bellman_Ford();

        cout << ( dis[ed] == INF ? -1 : dis[ed]) << endl;
    }
    return 0;
}

Bellman-flod 算法 + 队列优化 (spfa算法)

在Bellman-flod 算法中可以发现,很多松弛是无法进行的,只有当某一条边松弛之后,他的邻边才有可能松弛。

每次成功松弛的点入队,每次循环用已经松弛过的边松弛其他的边。

判断有无负环:如果某个点进入队列的次数超过一定次数则存在负环(在一次循环中可能会更新多次,但只入队一次,更新次数不能决定是否成环,因为一条边可以同时由许多其他边更新,一旦其他边更新,对于当前要检测是否成环的边的更新次数会超过 n - 1,入队或者出队次数才能代表该边的松弛情况,当它入队超过n - 1时,表明成环)。也可以利用Bellman-flod中检测负边权的方法。

期望的时间复杂度O(kE), 其中k为所有顶点进队的平均次数,可以证明k一般小于等于2。

SPFA 在形式上和宽度优先搜索(bfs)非常类似,不同的是宽度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。

SPFA算法有两个优化算法 SLF 和 LLL。

  • SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾。

  • LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。

  • 引用网上资料,SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。

#include<bits/stdc++.h>
#define PUSH(x,y,z) G[x].push(make_pair(y,z))

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e3 + 5;

typedef pair<int,int> P;
int n,m,st,ed;
int dis[N],arr[N][N],check[N];
bool vis[N];

inline void init()
{
    memset(vis,false,sizeof vis);
    memset(dis,INF,sizeof dis);
    memset(check,0,sizeof check);

    for( int i = 0;i < n; ++ i )
    {
        for( int j = i + 1;j < n; ++ j )
        {
            arr[i][j] = arr[j][i] = INF;
        }
        arr[i][i] = 0;
    }
}

void spfa()
{
    queue<int> q;
    vis[st] = true;
    dis[st] = 0;
    q.push(st);

    while(q.size())
    {
        int now = q.front();
        q.pop();
        vis[now] = false;

        for( int i = 0;i < n; ++ i )
        {
            if(dis[i] > dis[now] + arr[now][i])
            {
                dis[i] = dis[now] + arr[now][i];
                if(!vis[i])//把松弛过但不在队列中的边推进去,进而松弛其他边
                {
                    check[i] ++;
                    if(check[i] > n - 1 )
                    {
                        //cout << i << endl;
                        dis[ed] = INF;
                        break;
                    }
                    vis[i] = true;
                    q.push(i);
                }
            }
        }

    }

}

int main()
{
    int a,b,c;
    while(cin >> n >> m)
    {
        init();
        for( int i = 0;i < m; ++ i )
        {
            cin >> a >> b >> c;
            arr[a][b] = arr[b][a] = min(arr[a][b],c);
        }

        cin >> st >> ed;
        spfa();

        cout << (dis[ed] == INF ? -1 : dis[ed]) << endl;
    }
    return 0;
}

//邻接表写法
#include<bits/stdc++.h>
#define PUSH(x,y,z) G[x].push_back(make_pair(y,z))

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e3 + 5;

typedef pair<int,int> P;

int n,m,st,ed,dis[N],check[N];
bool vis[N];

vector<P> G[N];

inline void init()
{
    memset(dis,INF,sizeof dis);
    memset(vis,false,sizeof vis);
    memset(check,0,sizeof check);

    for( int i = 0;i < N; ++ i )G[i].clear();
}

void spfa()
{
    queue<int> q;
    q.push(st);
    dis[st] = 0;

    while(q.size())
    {
        int now = q.front();
        q.pop();
        vis[now] = false;//这里不能丢,如果vis[now]更新了,需要再次放入队列。
        for( int i = 0;i < G[now].size(); ++ i )
        {
            int v = G[now][i].first;
            if(dis[v] > dis[now] + G[now][i].second)
            {
                dis[v] = dis[now] + G[now][i].second;
                if(!vis[v])
                {
                    vis[v] = true;
                    q.push(v);
                }
            }

        }
    }
}

int main()
{
    int a,b,c;
    while(cin >> n >> m)
    {
        init();

        for( int i = 0;i < m; ++ i )
        {
            cin >> a >> b >> c;
            PUSH(a,b,c);
            PUSH(b,a,c);
        }

        cin >> st >> ed;
        spfa();

        cout << (dis[ed] == INF ? -1 : dis[ed]) << endl;
    }

    return 0;
}

前向星和链式前向星

前向星

是一种特殊的边集数组,我们把边集数组中的每一条边按照起点从小到大排序,如果起点相同就按照终点从小到大排序,

并记录下以某个点为起点的所有边在数组中的起始位置和存储长度,那么前向星就构造好了.

用len[i]来记录所有以i为起点的边在数组中的存储长度.

用head[i]记录以i为边集在数组中的第一个存储位置.

最终可以达到快速访问指定起点的边集。缺点需要排序,快排nlogn.

链式前向星

事实上根本不需要排序,我们的目的是在一堆数组中直接访问指定起点的边集,我们可以牺牲一些空间保存同一类集合的下标即可,对于每一条边需要保存其起点、终点、长度等。(如果是开太多数组,牺牲太多,又和邻接矩阵没差,小声bb)

对比:链式前向星和领接表的遍历速度都不如邻接矩阵,但存储的边的数量都比邻接矩阵要多,极端情况下邻接表可能会卡vector。

存边

int head[N];//一般初始化为-1

struct node{
    int next,to,w;
}edge[N];

写入

int cnt = 0;
void add(int u,int v,int w)
{
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];//指向上一条同起点的边
    head[u] = cnt ++;//更新head[u]为当前边
}

其中edge[i].to表示第i条边的终点,edge[i].next表示与第i条边同起点的下一条边的存储位置,edge[i].w为边权值.

另外还有一个数组head[],它是用来表示以i为起点的第一条边存储的位置,实际上你会发现这里的第一条边存储的位置其实

在以i为起点的所有边的最后输入的那个编号.

整数快读模板

int rd()
{
    int res = 0,f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){
        if(ch == '-')f = -1;
        ch = getchar();
    }
    
    while(ch >= '0' && ch <= '9'){
        res = res * 10 + ch - '0';
        ch = getchar();
    }
    
    return f * res;
}

链式前向星 模板题 洛谷P4479

#include<bits/stdc++.h>

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e5 + 5;
const int M = 2e5 + 5;

typedef pair<int,int> P;

struct node{
    int next,to,w;
}edge[M];

int n,m,st,ed,cnt;
int head[N],dis[N];
bool vis[N];

inline void init()
{
    cnt = 0;
    memset(head,-1,sizeof head);
    memset(dis,INF,sizeof dis);
    memset(vis,false,sizeof vis);
}

int rd()
{
    int res = 0,f = 1;
    char ch = getchar();

    while(ch < '0' || ch > '9')
    {
        if(ch == '-')f = -1;
        ch = getchar();
    }

    while(ch <= '9' && ch >= '0')
    {
        res = res * 10 + ch - '0';
        ch =getchar();
    }

    return f * res;
}

inline void add(int u,int v,int w)
{
    edge[cnt].to = v;
    edge[cnt].w = w;
    edge[cnt].next = head[u];
    head[u] = cnt ++;
}

void dijkstra()
{
    priority_queue<P,vector<P>,greater<P> > q;
    dis[st] = 0;
    q.push(make_pair(0,st));

    while(q.size())
    {
        P now = q.top();
        q.pop();

        int u = now.second;
        if(vis[u])continue;
        vis[u] = true;

        for( int i = head[u]; ~ i;i = edge[i].next)
        {//因为head初始化为-1,当i = -1 时结束,因为-1存储形式为111……111 按位去反后为0,所以可以写作 ~ i
            int v = edge[i].to;
            if(!vis[v] && dis[v] > dis[u] + edge[i].w)
            {
                dis[v] = dis[u] + edge[i].w;
                q.push(make_pair(dis[v],v));
            }
        }
    }
}

int main()
{
    int a,b,c;
    init();

    n = rd();
    m = rd();
    st = rd();

    for( int i = 0;i < m; ++ i ){
        a = rd();
        b = rd();
        c = rd();
        add(a,b,c);
    }

    dijkstra();
    for( int i = 1;i <= n; ++ i ){
        printf("%d ",dis[i]);
    }
    puts("");

    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肥羊也

感谢给肥羊投喂!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值