最短路(Dijkstra、bellman-ford、spfa、Floyd)

集训第二章,最短路。

o(TヘTo)


目录

Dijkstra

bellman-ford

spfa

Floyd


最短路算法用于求出从给定一个点V_{i}到任一个点V_{j}的最短路径长度。

Dijkstra

给定路径从V_{i}V_{j},路径的边权为W。 

Dijkstra算法是用于求边权非负时(W\geq 0)从起点到任意点的最短路。

Dijkstra算法的基本思想是从V_{i}出发,逐步地向外探寻最短路径。执行过程中,每个结点记录从起点到这个点的最短路的值,算法的每一步是去修改结点的数值。

void dijkstra()
{
    memset(dis , INF , sizeof(dis)) ;//将dis初始化为正无穷
    dis[1] = 0 ; //初始1号点的dis为0,
    for(int i = 0 ; i < n-1 ; i ++)
    {
        int t = -1 ; //t记录下一个要走的点
        for(int j = 1 ; j <= n ; j ++)
            if(!visited[j] && (dis[t] > dis[j] || t == -1))
                t = j ;  //每次找出最短的路径
        for(int j = 1 ; j <= n ; j ++)
            dis[j] = min(dis[j] , dis[t]+g[t][j]) ; //dis[j]记录的是从1号点到j的最短路
        visited[t] = true ; //标记已经走过的点
    }
}

画个图来举栗子~

如图五个点六条边,边权如图所示,求点1到点5的最短路。 

首先,将点1初始化:dis\left [ 1 \right ]=0

起始从点1开始,可以走向点2或点4,则点2和点4的值将被更新,即为点1的值加上从1-4的边权。第二次选取点2,可以走向点3和点4,点3的值为点2的值加上2-3的权,即dis\left [ 3 \right ]=dis\left [ 2 \right ]+g\left [ 2 \right ]\left [ 3 \right ];而此时的dis\left [ 2 \right ]+g\left [ 2 \right ]\left [ 4 \right ]=3小于第一次更新的点4的值,则此时的点4应为:dis\left [ 4 \right ]=min\left (dis\left [ 2 \right ]+g\left [ 2 \right ]\left [ 4 \right ] ,dis\left [ 4 \right ] \right )

第三次选取点4,从点4到点5,点5的值被更新为 dis\left [ 4 \right ]+g\left [ 4 \right ]\left [ 5 \right ]=6

最后选取点3,从点4到点5,点5的值被更新为 min\left ( dis\left [ 3 \right ]+g\left [3 \right ]\left [ 5 \right ],dis\left [ 5 \right ] \right )=5

最终从点1到点5的最短路的值为5。

下图为dis\left [ j \right ]数组的更新过程,也就是每个结点数值的更新,每次更新为最小值,最终得到最小的dis\left [ n \right ]

 

 接下来一个模板题,给一个有向图,给出各个边,求从1号点到n号点的最短路,

#include <iostream>
#include <algorithm>
#include <cstring>
#define INF 0x3f3f3f3f

using namespace std ;

const int N = 1e5+10 ;
bool visited[N] ;
int dis[N] ;
int n , m ;
int g[1010][1010] ;

int dijkstra()
{
    memset(dis , INF , sizeof(dis)) ;
    dis[1] = 0 ;
    for(int i = 0 ; i < n-1 ; i ++)
    {
        int t = -1 ;
        for(int j = 1 ; j <= n ; j ++)
            if(!visited[j] && (dis[t] > dis[j] || t == -1))
                t = j ;
        for(int j = 1 ; j <= n ; j ++)
            dis[j] = min(dis[j] , dis[t]+g[t][j]) ;
        visited[t] = true ;
    }
    return dis[n] ;
}

int main()
{
    cin >> n >> m ;
    memset(g , INF , sizeof(g)) ;
    for(int i = 0 ; i < m ; i ++)
    {
        int x , y , z ;
        cin >> x >> y >> z ;
        g[x][y] = min(z , g[x][y]) ;
    }
    cout << dijkstra() << endl ;
    return 0 ;
}

bellman-ford

bellman_ford算法能找到从某个结点出发到所有结点的最短路(或者某些最短路不存在),支持负权

用结构体来维护每条路径:

struct node{
    int u , v , w ;
} ed[N] ;

bellman_ford算法代码实现如下~

void bellman_ford()
{
    memset(dis , 0x3f3f3f3f , sizeof(dis)) ;
    dis[1] = 0 ;//从点1开始走
    for(int i = 0 ; i < k ; i ++)
    {
        memcpy(last , dis , sizeof(dis)) ;//记录上一次的路径值
        for(int j = 0 ; j < m ; j ++)
        {
            node e = ed[j] ;
            dis[e.v] = min(dis[e.v] ,last[e.u]+e.w) ;//存在u到v的路,v的值为最小的u的值加u到v的权。
        }
    }
}

还是用这个图来研究bellman_ford算法~

每次循环都是走的当前可走的下一条路径,也就是:第一次走可以走到点2和点4,更新点2和4的值;第二次走可以走到点3、4、5,更新点3、4、5的值;第三次走可以走到5,更新点5的值。

 

画个每个点值的更新表~

求从点1到点n的最短路径,以下板子:

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

using namespace std ;

const int N = 10010 ;
int n , m ;
struct node{
    int u , v , w ;
} ed[N] ;
int visited[N] ;
int dis[N] , last[N] ;

int bellman_ford()
{
    memset(dis , 0x3f3f3f3f , sizeof(dis)) ;
    dis[1] = 0 ;
    for(int i = 0 ; i < m ; i ++)
    {
        memcpy(last , dis , sizeof(dis)) ;
        for(int j = 0 ; j < m ; j ++)
        {
            node e = ed[j] ;
            dis[e.v] = min(dis[e.v] ,last[e.u]+e.w) ;
        }
    }
    return dis[n] ;
}

int main()
{
    cin >> n >> m ;
    for(int i = 0 ; i < m ; i ++)
    {
        int x , y , z ;
        cin >> x >> y >> z ;
        ed[i].u = x , ed[i]. v = y ;
        ed[i].w = z ;
    }
    bellman_ford() ;
    if(dis[n] > 0x3f3f3f3f/2)   cout << "impossible\n" ;
    else    cout << dis[n] << endl ;
    return 0 ;
}

若为有边数限制的最短路,例如从点1到点n最多经过k条边的最短路,也可以用bellman_ford算法来求。

因为bellman_ford算法第k次循环记录的是从起点走k步后每个结点的值,所以循环k次后,所求点的dis值即为最多走k条边的最短路。(算法内部都是相同的。)

//有边数限制的最短路,
int bellman_ford()
{
    memset(dis , 0x3f3f3f3f , sizeof(dis)) ;
    dis[1] = 0 ;
    for(int i = 0 ; i < k ; i ++)
    {
        memcpy(last , dis , sizeof(dis)) ;
        for(int j = 0 ; j < m ; j ++)
        {
            node e = ed[j] ;
            dis[e.v] = min(dis[e.v] ,last[e.u]+e.w) ;
        }
        //for(int j = 1 ; j <= n ; j ++)  cout << dis[j] << " " ;
        //cout << endl ; 
    }
    return dis[n] ;
}

此外判断环或是判断负权环也能用bellman_ford算法。

这里有一个栗子,

1860 -- Currency Exchange (poj.org)

题意:

n、m、s、v分别为货币数量,兑换方式的数量,拥有的货币,拥有的货币面额。求是否可以通过兑换货币来增加财富。

思路:

图为有向图。每个货币就是一个点,将现在拥有的货币看成起始点,每一个兑换方式是一条连接两种货币的路,兑换汇率为边权。

判断是否有环,且环的值为正。当有正环时,则说明可以通过兑换货币,最终换回起初的货币时,使货币的数量增加。

复杂度 O\left ( NM \right ) ;

#include <iostream>
#include <algorithm>
#include <cstring>
#define INF 0x3f3f3f3f

using namespace std;

int n, m, s;
double v;
const int N = 1100;
double dis[N];
struct node
{
    int a, b;
    double r, c;
} p[N];

bool bellman_ford(int M)
{
    for (int i = 1; i <= n; i++)
        dis[i] = 0.0; //初始化dis数组
                      //dis用来记录兑换的货币面额
    dis[s] = v;//初始化,起始拥有货币s的数量为v;
    for (int i = 0; i < n; i++)
    {
        bool flag = 0;
        for (int j = 0; j < M; j++)
        {
            node u = p[j];
            if (dis[u.b] < (dis[u.a] - u.c) * u.r) //如果a兑换成b后的面额大于b之前一次的面额;
            {
                flag = 1;
                dis[u.b] = (dis[u.a] - u.c) * u.r;
            }
        }
        if (!flag)//如果到某个货币它不能兑换出面额增加的货币。
            return false;
    }
    for (int i = 0; i < M; i++)
    {
        node u = p[i];
        if (dis[u.b] < (dis[u.a] - u.c) * u.r)//如果有兑换后面额增加的货币
            return true;
    }
    return false;
}

int main()
{
    cin >> n >> m >> s >> v;
    int j = 0; //兑换情况的数量
    for (int i = 0; i < m; i++)
    {
        int a, b;
        cin >> a >> b;
        double rab, cab, rba, cba;
        cin >> rab >> cab >> rba >> cba;
        p[j].a = a, p[j].b = b;
        p[j].r = rab, p[j].c = cab;
        j++;
        p[j].a = b, p[j].b = a;
        p[j].r = rba, p[j].c = cba;
        j++;
    }
    if (bellman_ford(j))
        cout << "YES\n";
    else
        cout << "NO\n";
    return 0;
}

spfa

spfa算法是bellman_ford的升级版本,用到优先队列来优化。

直接写模板题代码:

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

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool visited[N];

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

int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    visited[1] = true;

    while (q.size())
    {
        int t = q.front();
        q.pop();

        visited[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 (!visited[j])
                {
                    q.push(j);
                    visited[j] = true;
                }
            }
        }
    }

    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 == 0x3f3f3f3f) cout << "No Shortest Path" << endl;
    else cout << t << endl ;

    return 0;
}

用spfa算法判断负权环:

bool spfa() //spfa算法判断是否存在负环
{
    queue<int> q;

    for (int i = 1; i <= n; i ++ )
    {
        visited[i] = true;
        q.push(i);
    }

    while (!q.empty())
    {
        int t = q.front();
        q.pop();

        visited[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 true;
                if (!visited[j])
                {
                    q.push(j);
                    visited[j] = true;
                }
            }
        }
    }
    return false;
}

Floyd

Floyd算法用来求任意两点间的最短路。是一种暴力求解法,复杂度O\left ( N^{3} \right ) 。

只要最短路存在,Floyd算法适用于任何图(只要能跑动三个for循环.jpg)。

其核心思想:g[i][j] = min(g[i][k]+g[k][j] , g[i][j]),也就是从i到j的最短路为从i到k的最短路加上从k到j的最短路 ,两者取最小。其实就是用了三重循环,,

void floyd()
{
    for(int k = 1 ; k <= n ; k ++)
        for(int i = 1 ; i <= n ; i ++)
            for(int j = 1 ; j <= n ; j ++)
                dis[i][j] = min(dis[i][j] , dis[i][k]+dis[k][j]) ;
} //floyd求最短路

(贼喜欢这个算法,贼好理解,贼好写,虽然它贼暴力。)

求任意两点间的最短路,板子:

#include <iostream>
#include <algorithm>
#include <cstring>
#define INF 0x3f3f3f3f

using namespace std ;

int m , n , k ;
const int N = 2e4+10 ;
int dis[N][N] ;

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

int main()
{
    cin >> n >> m >> k ;
    for(int i = 1 ; i <= n ; i ++)
        for(int j = 1 ; j <= n ; j ++)
        {
            if(i == j)  dis[i][j] = 0 ;
            else    dis[i][j] = INF ;
        }
    for(int i = 0 ; i < m ; i ++)
    {
        int x , y , z ;
        cin >> x >> y >> z ;
        dis[x][y] = min(z , dis[x][y]) ;
    }
    floyd() ;
    while(k--) 
    {
        int l , r ;
        cin >> l >> r ;
        if(dis[l][r] > INF/2)   cout << "No Shortest Path\n" ;
        else cout << dis[l][r] << endl ;
    }
    return 0 ;
}

在郑州经历了一场百年一遇的大雨之后,一周没有学习算法的我,来电后的第一天就拿出键盘把我的最短路火速总结了一下。求求灾情快过去,一切恢复正常运转 ~~~ 

依然要好好学习啊( ̄︶ ̄)~~~ 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值