zoukankan      html  css  js  c++  java
  • 最短路

    上边n表示节点个数,m表示边,其中稠密图用邻接矩阵存,稀疏图用邻接表来存。

    稀疏图与稠密图定义:数据结构中对于稀疏图的定义为:有很少条边或弧(边的条数|E|远小于|V|²)的图称为稀疏图(sparse graph),反之边的条数|E|接近|V|²,称为稠密图(dense graph)。(来自百度百科)

    所有边权都是正数

    Dijkstra算法

    1.朴素Dijkstra算法

    模板

    s数组:标记当前已经确定最短距离的点
    void Dijkstra()
    {
          dist[1] = 0, dist[i] =  +$infty$  //初始化
          for i: 0 ~ n //n个点
                for j;1 ~ n
                t <- 不在s中的,距离最近的点
                st[t] = true; //标记
                for j;1 ~ n
                用t更新其他的点的距离
    }
    

    例题

    给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。

    请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。

    输入格式
    第一行包含整数n和m。

    接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

    输出格式
    输出一个整数,表示1号点到n号点的最短距离。

    如果路径不存在,则输出-1。

    数据范围
    1 ≤ n ≤ 500,
    1 ≤ m ≤ 10^5,
    图中涉及边长均不超过10000。

    输入样例:
    3 3
    1 2 2
    2 3 1
    1 3 4
    输出样例:
    3

    首先题目中说明存在重边和自环,我们用邻接矩阵来存图必须要解决这个问题,因此我们做这样的决策:对于重边,取最小;对于自环,忽略不计,设为0。
    因此可以这样:1.将g数组全部初始化为0 2.g[a][b] = min(g[a][b], c)

    根据上边稀疏图与稠密图定义可知:这是一个稠密图,因此用邻接矩阵来存图

    代码

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 510;
    
    int n, m;
    int g[N][N]; //邻接矩阵存图
    int dist[N]; //图中每个点到1的距离
    bool st[N]; //表示某个点的最短距离是否被确定
    
    int Dijkstra()
    {
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
    
        for(int i = 0; i < n; i++) //确定n个点的最短距离 确定好每个点到起点的最短距离 
        {
            int t = -1;
            for(int j = 1; j <= n; j++) //寻找不在st中 距离最近的点
                if(!st[j] && (t == -1 || dist[t] > dist[j])) //在还没有确定的点中找到距离1最近的点
                    t = j;
            
            for(int j = 1; j <= n; j++)  //用t更新其他点的距离
            { 
                dist[j] = min(dist[j], dist[t] + g[t][j]);  //用1~t+t~j的长度更新1~j的长度
            }
    
            st[t] = true;  //表示已经t这个点已经确定最短距离
        }
    
        if(dist[n] == 0x3f3f3f3f) return -1; //表示1与n不连通
        return dist[n];
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
    
        memset(g, 0x3f, sizeof g);
        while(m--)
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
    
            g[a][b] = min(g[a][b], c); //用邻接矩阵来存图   
        }
    
        printf("%d
    ", Dijkstra());
    
        return 0;
    }
    

    2.堆优化Dijkstra算法

    看这个模板

    s数组:标记当前已经确定最短距离的点
    void Dijkstra()
    {
          dist[1] = 0, dist[i] =  +$infty$  //初始化
          for i: 0 ~ n //n个点
                for j;1 ~ n
                **t <- 不在s中的,距离最近的点
                st[t] = true; //标记
                for j;1 ~ n
                用t更新其他的点的距离
    }
    

    其中加**的那一步,我们寻找最小点的过程遍历了所有的边,很费时间,所以我们可以直接用优先队列定义一个小根堆弹出距离1最小的节点(即堆顶), 省略了查找的过程,更新后的节点则重新压入队列尾部。

    堆优化后的Dijkstra代码:(相比于上题更改了数据范围,1 ≤ n,m ≤ 1.5×10^5 是一个稀疏图,因此用邻接表来存图)

    int Dijkstra()
    {
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
        priority_queue<PII, vector<PII>, greater<PII>> heap; //创建小根堆
        heap.push({0, 1});   //second是编号 first是距离 因为根据优先队列会先根据第一个关键字排序 因此这样设定
    
        while(heap.size())
        {
            auto t = heap.top();
            heap.pop();
    
            int var = t.second; //记录编号
            
            if(st[var]) continue; //如果已经确定距离 则continue
            st[var] = true; //标记
    
            for(int i = h[var]; i != -1; i = ne[i]) //遍历当前点的所有出边  因为是稀疏图用邻接表来存储
            {
                int j = e[i];
                if(dist[j] > dist[var] + w[i]) //更新
                {
                    dist[j] = dist[var] + w[i];
                    heap.push({dist[j], j});
                }
            }
        }
    
        if(dist[n] == 0x3f3f3f3f) return -1; //说明没被更新
        return dist[n];
    }
    

    用邻接表存图不需要考虑重边和自环

    总代码

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    
    using namespace std;
    
    typedef pair<int, int> PII; //first表示距离 second表示编号
    
    const int N = 1e6+10;
    int n, m;
    int h[N], e[N], ne[N], w[N], idx;
    int dist[N];
    bool st[N];
    
    void add(int a, int b, int c)
    {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; //稀疏图用邻接表来存
    }
    
    int Dijkstra()
    {
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
        priority_queue<PII, vector<PII>, greater<PII>> heap; //创建小根堆
        heap.push({0, 1});  
    
        while(heap.size())
        {
            auto t = heap.top();
            heap.pop();
    
            int var = t.second; //记录编号
            
            if(st[var]) continue;
            st[var] = true;
    
            for(int i = h[var]; i != -1; i = ne[i]) //遍历当前点的所有出边
            {
                int j = e[i];
                if(dist[j] > dist[var] + w[i]) //更新
                {
                    dist[j] = dist[var] + w[i];
                    heap.push({dist[j], j});
                }
            }
        }
    
        if(dist[n] == 0x3f3f3f3f) return -1;
        return dist[n];
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
        
        memset(h, -1, sizeof h);
    
        while(m--)
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
    
            add(a, b, c);
        }
    
        printf("%d
    ", Dijkstra());
    
        return 0;
    }
    

    存在负权边

    Bellman-Ford算法

    处理存在有负权边求最短路的情况,但是如果存在负权回路则最短路就可能不存在,即-(infty),因此求最短路一般都没有负权回路,但是也不排除有负权回路的情况,例如这种:

    另外我们还可以求有边数限制的最短路,因为有边数限制,所以存在负权回路也没有关系。

    例题:求有边数限制的最短路

    给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。

    请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。

    注意:图中可能 存在负权回路 。

    输入格式
    第一行包含三个整数n,m,k。

    接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

    输出格式
    输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。

    如果不存在满足条件的路径,则输出“impossible”。

    数据范围
    1 ≤ n,k ≤ 500,
    1 ≤ m ≤ 10000,
    任意边长的绝对值不超过10000。

    输入样例:
    3 3 1
    1 2 1
    2 3 1
    1 3 3
    输出样例:
    3

    因此这种题的模板:

    backup数组使用来备份dist数组的
    void Bellman_Ford()
    {
          for k次   //不超过k条边的最短距离
                memcpy(backup, dist, sizeof dist); //备份 因为防止串联
                for 所有边 a,b,w
                      dist[b] = min(dist[b], backup[a] + w);
    }
    
    

    上面的串联是因为可能出现这种情况:



    这中情况下,如果我们限制边的条数为1,那么最短路就是3,而不是2,如果我们用dist[a] + w来更新dist,则会导致dist[3] = 2,即最短路距离为2,可与事实不符。
    因此我们采用第k次遍历时dist的备份,用备份来更新dist

    从1->2->3:
    备份:dist[1] = 0, dist[2] = +(infty), dist[3] = +(infty) (备份)
    更新:dist[1] = 0, dist[2] = min(+(infty), +(infty)+1) = +(infty), dist[3] = min(+(infty), +(infty)+1), 这样以来dist[2]dist[3]就不会被更新啦。

    从1->3:
    备份:dist[1] = 0, dist[3] = +(infty) (备份)
    则:dist[3] = min(dist[3], dist[1] + 3) = 3

    还有一个问题就是如何返回:
    没有找到路径返回-1,如果这样写的话:if(dist[n] > 0x3f3f3f3f) return -1,那就错了,因为负权边的存在,有的点可能被更新了,小于0x3f3f3f3f,因此这样写就错了。
    由此可见题目中数据范围因为500*10000,按极限最多500w,因此说最短路最多也就是0x3f3f3f3f-500w,不可能再大了,因此我们可以把条件设置成这样:if(dist[n] > 0x3f3f3f3f) return -1

    代码

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 512, M = 10010;
    
    struct Edge
    {
        int a, b, w;
    }edges[M];
    
    int n, m, k;
    int dist[N], backup[N];
    
    int Bellman_Ford()
    {
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
    
        for(int i = 0; i < k; i++) //最多经过k条边
        {
            memcpy(backup, dist, sizeof dist);  //备份一下
            for(int j = 0; j < m; j++) //一共m条边
            {
                int a = edges[j].a, b = edges[j].b, w = edges[j].w;
                dist[b] = min(dist[b], backup[a] + w); // 只用上一次迭代的结果来更新 这样就不会发生串联
            }    
        }
    
        if(dist[n] > 0x3f3f3f3f / 2) return -1; //由于存在负权边 因此在更新dist的时候可能会导致没有到过的点更新成小于0x3f3f3f3f 
        //因为数据是500*10000,最多五百万,因此最短路最多也得是0x3f3f3f3f-500w,不可能再大了,因此大于这个值说明一定不存在最短路
        return dist[n];
    }
    
    int main()
    {
        scanf("%d%d%d", &n, &m, &k);
    
        for(int i = 0; i < m; i++)
        {
            int a, b, w;
            scanf("%d%d%d", &a, &b, &w);
            edges[i] = {a, b, w};
        }
    
        int t = Bellman_Ford();
    
        if(t == -1) puts("impossible");
        else printf("%d
    ", dist[n]);
        return 0;
    }
    

    spfa算法


    这个算法优化了Bellman-ford算法,在更新的那一步,dist[b] = min(dist[b], backup[a] + w),我们用一个for循环更新了所有的边,其实没有必要这样,我们只需要每次更新一个节点的出边即可,因此这里就可以用一个队列来优化,队列里边存的时待更新的点。

    模板

    void spfa()
    {
          queue <- 1
          while 队列不空
             1.t <- front, q.pop()     
             2.更新t的所有出边 t->b: queue <- b
    }
    

    例题 1:求最短路

    给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。

    请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible。

    数据保证不存在负权回路。

    输入格式
    第一行包含整数n和m。

    接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

    输出格式
    输出一个整数,表示1号点到n号点的最短距离。

    如果路径不存在,则输出”impossible”。

    数据范围
    1≤n,m≤105,
    图中涉及边长绝对值均不超过10000。

    输入样例:
    3 3
    1 2 5
    2 3 -3
    1 3 4
    输出样例:
    2

    代码

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    
    using namespace std;
    
    const int N = 1e5+10;
    int n, m;
    int h[N], e[N], ne[N], w[N], idx;
    int dist[N];
    bool st[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);
        st[1] = true; //标记其在堆中
        
        while(q.size())
        {
            int t = q.front();
            q.pop();
            st[t] = false; //st数组的是否存在不影响程序的准确性 只通过节点是否重入队复影响程序的效率
    
            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()
    {
        scanf("%d%d", &n, &m);
    
        memset(h, -1, sizeof h);
    
        while(m--)
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
    
            add(a, b, c);
        }
    
        int t = spfa();
        if(t == -1) puts("impossible");
        else printf("%d
    ", t);
    
        return 0;
    }
    
    

    例题 2:判断负环

    抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。 抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。它是组合数学中一个重要的原理 。(来自百度百科)
    我们用一个cnt数组来计数,每遍历到一个节点就+1,这里假设有n个节点,如果出现cnt[j] >= n,那么说明有环,根据抽屉原理,cnt[j] = 11,则必有两个点是重合的,那么说明就一定存在一个环。


    给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
    请你判断图中是否存在负权回路。
    输入格式
    第一行包含整数n和m。

    接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

    输出格式
    如果图中存在负权回路,则输出“Yes”,否则输出“No”。

    数据范围
    1≤n≤2000,
    1≤m≤10000,
    图中涉及边长绝对值均不超过10000。

    输入样例:
    3 3
    1 2 -1
    2 3 4
    3 1 -4
    输出样例:
    Yes

    代码

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    
    using namespace std;
    
    const int N = 2010, M = 10010;
    int h[N], e[M], ne[M], w[M], idx;
    int n, m;
    int dist[N], cnt[N];
    bool st[N];
    
    void add(int a, int b, int c)
    {
        e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
    }
    
    bool spfa()
    {
        queue<int> q;
    
        for(int i = 1; i <= n; i++) //所有点入栈 因为环不一定从1开始
        {
            q.push(i);
            st[i] = true;
        }
    
        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 true; //说明出现负环
                    if(!st[j])
                    {
                        st[j] = true;
                        q.push(j);
                    }
                }
            }
        }
    
        return false;
    }
    
    int main()
    {
        cin >> n >> m;
    
        memset(h, -1, sizeof h);
    
        while(m--)
        {
            int a, b, c;
            cin >> a >> b >> c;
    
            add(a, b, c);
        }
    
        if(spfa()) puts("Yes");
        else puts("No");
    
        return 0;
    }
    

    Floyd算法

    用来求多元最短路,即任意两个点的最短路经。
    据说这是求最短路最简单的算法,基于动态规划,原理我还不是很明白,因此只能等学完动态规划后再回来看吧。

    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 210, INF = 1e9;
    int n, m, Q;
    int d[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++)
                {
                    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;
    
            int t = d[a][b];
            if(t > INF / 2) puts("impossible");
            else cout << t << endl;
        }
    
        return 0;
    }
    
  • 相关阅读:
    洛谷p1017 进制转换(2000noip提高组)
    Personal Training of RDC
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Eurasia
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Peterhof.
    Asia Hong Kong Regional Contest 2019
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Siberia
    XVIII Open Cup named after E.V. Pankratiev. Ukrainian Grand Prix.
    XVIII Open Cup named after E.V. Pankratiev. GP of SPb
    卜题仓库
    2014 ACM-ICPC Vietnam National First Round
  • 原文地址:https://www.cnblogs.com/ZhengLijie/p/13423119.html
Copyright © 2011-2022 走看看