图论-最短路问题

yxy 发布于 2025-03-23 最后更新于 2025-03-28 372 次阅读


声明:

本文仅为作者为了复习而写,并非作为教程使用!

常见的最短路算法:

常见的最短路算法一般有:BFS、DFS、Floyd、Dijkstra、Bellman-Ford、SPFA、Johnson。

常见的存图方式:

1.邻接表 2.链式前向星(习惯)

1..邻接表

优点:1.空间复杂度低,适合存储稀疏图(边数较少的图)。2.插入和删除边的操作效率较高。

缺点:1.历遍所有边时,需要遍历每个顶点的链表,效率较低。2.不适合存储稠密图(边数接近顶点数的平方)。

适合场景:存储边数较少的图,如社交网络、交通网络等。

2..链式前向星

优点:1.效率高,所有数据存储在数组中,访问速度快。2.适合存储稀疏图,同时支持高效的边的插入和遍历。

缺点:1.实现相对复杂,需要维护多个数组。2.对于稠密图,空间浪费可能较大。

适合场景:适合存储边数较多但仍然稀疏的图,如大规模的网络拓扑结构。

总结:

  • 邻接表:适合稀疏图,实现简单,插入和删除边效率高。
  • 链式前向星:适合稀疏图,空间效率高,支持快速遍历和插入边。

最短路基础算法(大多数收集为板子)

1.租用游艇(福建夏令营) 普及-

开胃小菜

  • 经典的最短路,不过半矩阵的存图方式需要构思一下,类似一个对角矩阵,抽象成上三角矩阵来存图。邻接表时间复杂度O(n²) ,链式前向星(堆优化dijkstra) 时间复杂度O(nlogn)

题目描述

  • 长江游艇俱乐部在长江上设置了 n 个游艇出租站 1,2,⋯,n。游客可在这些游艇出租站租用游艇,并在下游的任何一个游艇出租站归还游艇。游艇出租站 i 到游艇出租站 j 之间的租金为 r(i,j)(1≤i<j≤n)。试设计一个算法,计算出从游艇出租站 1 到游艇出租站 n 所需的最少租金。
    输入格式:第一行中有一个正整数 n,表示有 n 个游艇出租站。接下来的 n−1 行是一个半矩阵 r(i,j)(1≤i<j≤n)。
    输出格式:输出计算出的从游艇出租站 1 到游艇出租站 n 所需的最少租金。

示例

  •  输入样例: 3 5 15 7 输出样例:12 解释: 第一行的数字 3 表示有 3 个游艇出租站。接下来的两行表示租金信息: 第二行的数字 5 和 15 表示从出租站 1 到出租站 2 的租金是 5,从出租站 1 到出租站 3 的租金是 15。 第三行的数字 7 表示从出租站 2 到出租站 3 的租金是 7。 我们需要找到从出租站 1 到出租站 3 的最少租金。我们可以选择直接从出租站 1 到出租站 3,租金是 15。 或者,我们可以选择从出租站 1 到出租站 2,然后从出租站 2 到出租站 3,总租金是 5 + 7 = 12。 因此,从出租站 1 到出租站 3 的最少租金是 12.

--实现方式

  1. 邻接矩阵实现

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <queue>
    #include <unordered_map>
    #include <map>
    #include <cstring>
    #include <cmath>
    #include <unordered_set>
    #include <set>
    #include <utility>
    #include <climits>
    
    #define int long long
    #define PII pair<int, int>
    #define inf 0x3f3f3f
    #define all(v) (v.begin() , v.end())
    
    using namespace std;
    const int N = 220;
    
    int n;
    int dist[N] , g[N][N];
    bool st[N];
    
    int dijkstra()
    {
        memset(dist , 0x3f , sizeof dist);
    
        dist[1] = 0;
        
        for (int i = 1 ; i <= n ; i ++ ) 
        {
            int t = -1;
            for (int j = 1 ; j <= n ; j ++ )
                if (!st[j] && (t == -1 || dist[t] > dist[j]))
                    t = j;
            st[t] = true;
    
            for (int j = 1 ; j <= n ; j ++ )
                dist[j] = min(dist[j] , dist[t] + g[t][j]);
        }
    
        return dist[n];
    }
    
    void solve() 
    {
        cin >> n;
    
        memset(g , 0x3f , sizeof g);
    
        for (int i = 1 ; i < n ; i ++ ) 
        {
            for (int j = i + 1 ; j <= n ; j ++ ) 
            {
                cin >> g[i][j];
            }
        }
    
        cout << dijkstra() << endl;
    
    }
    
    signed main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr), cout.tie(nullptr);
    
        solve();
        return 0;
    }
    
  2. 链式前向星实现

    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <queue>
    #include <unordered_map>
    #include <map>
    #include <cstring>
    #include <cmath>
    #include <unordered_set>
    #include <set>
    #include <utility>
    #include <climits>
    
    // #define int long long
    #define PII pair<int, int> 
    #define inf 0x3f
    #define all(v) v.begin() , v.end()
    
    using namespace std;
    const int N = 1e6  + 10;
    
    int e[N] , w[N] , ne[N] , dist[N] , h[N] , idx;
    int 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 ++;
    }
    
    void dijkstra()
    {
        memset(dist , 0x3f , sizeof dist);
        memset(st , false , sizeof st);
        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 distance = t.first , index = t.second;
    
            if (st[index]) continue;
            st[index] = true;
    
            for (int i = h[index] ; i != -1 ; i = ne[i])
            {
                int j = e[i];
                if (dist[j] > distance + w[i])
                {
                    dist[j] = distance + w[i];
                    heap.push({dist[j] , j});
                }
            }
        }
    }
    
    void solve()  
    {   
        memset(h , -1 , sizeof h);
        cin >> n;
        for (int i = 1 ; i < n ; i ++ ) 
        {
            for (int j = i + 1 ; j <= n ; j ++ )
            {
                int distance;
                cin >> distance;
                add(i , j , distance);
            }
        }    
    
        dijkstra();
    
        cout << dist[n] << endl;
    }
    
    signed main() 
    {
        ios::sync_with_stdio(false);
        cin.tie(nullptr), cout.tie(nullptr);
    
        solve();
        return 0;
    }

dp解法:

下次再补>-..-<

2.单源最短路模板(dijkstra堆优化模板) 提高-

  • dijkstra堆优化版时间复杂O(nlogn) , 可过1e5 以上的数据。同样可使用朴素版dijkstra和spfa 时间复杂度O(n²)。1e5数据会被卡。

题目描述

  • 给定一个 n 个点,m 条有向边的带非负权图,请你计算从 s 出发,到每个点的距离。数据保证你能从 s 出发到任意点。
    输入格式:第一行为三个正整数 n,m,s。 第二行起 m 行,每行三个非负整数 u i ​ ,v i ​ ,w i ​ ,表示从 u i ​ 到 v i ​ 有一条权值为 w i ​ 的有向边。
    输出格式:输出一行 n 个空格分隔的非负整数,表示 s 到每个点的距离。

示例

  • 输入:
    4 6 1
    1 2 2
    2 3 2
    2 4 1
    1 3 5
    3 4 3
    1 4 4
  • 输出:
    0 2 4 3

代码

const int N = 1e6 + 10;
int e[N] , ne[N] , h[N] , w[N] , dist[N] , idx;

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

void dijkstra(int s)
{
    memset(dist , 0x3f , sizeof dist);
    memset(st , false , sizeof st);

    dist[s] = 0;
    priority_queue<PII , vector<PII> , greater<PII> > heap;
    heap.push({0 , s});

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int distance = t.first , index = t.second;

        if (st[index]) continue;
        st[index] = true;

        for (int i = h[index] ; ~i ; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j] , j});
            }
        }
    }
}

void solve()
{
    memset(h , -1 , sizeof h);
    int n , m , s;
    cin >> n >> m >> s;

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

    dijkstra(s);
    
    for (int i = 1 ; i <= n ; i ++ )
        cout << dist[i] << " ";
}

3.负环(spfa) 提高-

  • 记录每次更新最短路劲的个数,如果更新大于等于n 则说明出现了负环。大于n则说明有至少一个点使用了多次,使用多次只能是每次更新最短路径的适合经过他都能更小,那么只能是负环。使用spfa优化计数时间复杂度 O(n²)

题目描述

  • 给定一个 n 个点的有向图,请求出图中是否存在从顶点 1 出发能到达的负环。 负环的定义是:一条边权之和为负数的回路
    输入格式 :本题单测试点有多组测试数据。 输入的第一行是一个整数 T,表示测试数据的组数。对于每组数据的格式如下:
    第一行有两个整数,分别表示图的点数 n 和接下来给出边信息的条数 m。 接下来 m 行,每行三个整数 u,v,w。 若 w≥0,则表示存在一条从 u 至 v 边权为 w 的边,还存在一条从 v 至 u 边权为 w 的边。 若 w<0,则只表示存在一条从 u 至 v 边权为 w 的边。
    输出格式: 对于每组数据,输出一行一个字符串,若所求负环存在,则输出 YES,否则输出 NO

示例

  • 输入:
    2
    3 4
    1 2 2
    1 3 4
    2 3 1
    3 1 -3
    3 3
    1 2 3
    2 3 4
    3 1 -8
  • 输出:
    NO
    YES

代码

const int N = 1e4 + 10 , M = 4 * N;
int e[M] , ne[M] , h[N] , w[M] , dist[M] , idx;
bool st[M];
int cnt[M];
int n , m;

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

bool spfa()
{
    memset(dist , 0x3f , sizeof dist);
    memset(st , false , sizeof st);
    memset(cnt , 0 , sizeof cnt);

    queue<int> q;
    q.push(1);
    st[1] = true;
    dist[1] = 0;

    while (q.size())
    {
        auto 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])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

void solve()
{
    int t;
    cin >> t;
    while (t -- )
    {
        memset(h , -1 , sizeof h);
        cin >> n >> m;
        for (int i = 1 ; i <= m ; i ++ )
        {
            int a , b , c;
            cin >> a >> b >> c;
            add(a , b , c);
            if (c >= 0) add(b , a , c);
        }

        if (spfa()) cout << "YES" << endl;
        else cout << "NO" << endl;
    }
}

4.最短路计数 提高/普及+

类spfa做法同时带一点dp思想。当前跟新的最短路径是由上一个位置为终点转移而来的。只需要依次更新即可。放在这个是为了给以后有找最短路的个数留一个板子。时间复杂度O(n + m) 

题目描述

  • 给出一个 N 个顶点 M 条边的无向无权图,顶点编号为 1∼N。问从顶点 1 开始,到其他每个点的最短路有几条。
    输入格式:第一行包含 2 个正整数 N,M,为图的顶点数与边数。 接下来 M 行,每行 2 个正整数 x,y,表示有一条连接顶点 x 和顶点 y 的边,请注意可能有自环与重边。
    输出格式:共 N 行,每行一个非负整数,第 i 行输出从顶点 1 到顶点 i 有多少条不同的最短路,由于答案有可能会很大,你只需要输出 ans mod  100003 后的结果即可。如果无法到达顶点 i 则输出 0。

示例

  • 输入:
    5 7
    1 2
    1 3
    2 4
    3 4
    2 3
    4 5
    4 5
  • 输出:
    1
    1
    1
    2
    4

代码

const int mod = 1e5 + 3 , N = 1e6 + 10 , M = 4 * N;
int e[M] , ne[M] , h[N] , idx;
int cnt[M] , dist[M]; 
int n , m;

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

void bfs()
{
    memset(dist , -1 , sizeof dist);
    queue<int> q;

    dist[1] = 0 , cnt[1] = 1;
    q.push(1);

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

        for (int i = h[index] ; ~i ; i = ne[i])
        {
            int j = e[i];

            if (dist[j] == -1)
            {
                dist[j] = dist[index] + 1;
                cnt[j] = cnt[index];
                q.push(j);
            }
            else if (dist[j] == dist[index] + 1)
            {
                cnt[j] += cnt[index];
                cnt[j] %= mod;
            }
            
        }
    }

}

void solve()
{
    memset(h , -1 , sizeof h);
    cin >> n >> m;
    for (int i = 1 ; i <= m ; i ++ )
    {
        int a , b;
        cin >> a >> b;
        add(a , b) , add(b , a);
    }

    bfs();

    for (int i = 1 ; i <= n ; i ++ ) cout << cnt[i] << endl;
}