最短路

上边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;
}
原文地址:https://www.cnblogs.com/ZhengLijie/p/13423119.html