• 学习笔记-最短路


    觉得讲的不详细可以去B站看:https://www.bilibili.com/video/av85550343

    1. 正权无向图最小环问题(floyd)

    引用链接:点我


        抛开Dijkstra算法,进而我们想到用Floyd算法。我们知道,Floyd算法在进行时会不断更新矩阵dist(k)。设dist[k,i,j]表示从结点i到结点j且满足所有中间结点,它们均属于集合{1,2,⋯ ,k}的一条最短路径的权。其中dist[0,i,j ]即为初始状态i到j的直接距离。对于一个给定的赋权有向图, 求出其中权值和最小的一个环。我们可以将任意一个环化成如下形式:u->k->v ->(x1-> x2-> ⋯ xm1)-> u(u与k、k与v都是直接相连的),其中v ->(x1-> 2-> ⋯ m)-> u是指v到u不经过k的一种路径。

        在u,k,v确定的情况下,要使环权值最小, 则要求 (x1一>x2->⋯一>xm)->u路径权值最小.即要求其为v到u不经过k的最短路径,则这个经过u,k,v的环的最短路径就是:[v到u不包含k的最短距离]+dist[O,u,k]+dist[O,k,v]。我们用Floyd只能求出任意2点间满足中间结点均属于集合{1,2,⋯ ,k}的最短路径,可是我们如何求出v到u不包含k的最短距离呢?
        现在我们给k加一个限制条件:k为当前环中的序号最大的节点(简称最大点)。因为k是最大点,所以当前环中没有任何一个点≥k,即所有点都<k。因为v->(x1->x2->......xm)->u属于当前环,所以x1,x2,⋯ ,xm<k,即x1,x2.⋯。xm≤k一1。这样,v到u的最短距离就可以表示成dist[k一1 ,u,v]。dist[k一1,v,u]表示的是从v到u且满足所有中间结点均属于集合{1,2,⋯ ,k一1}的一条最短路径的权。接下来,我们就可以求出v到u不包含k的最短距离了。这里只是要求不包含k,而上述方法用的是dist[k一1,v,u],求出的路径永远不会包含k+l,k+2,⋯ 。万一所求的最小环中包含k+1,k+2,⋯ 怎么办呢?的确,如果最小环中包含比k大的节点,在当前u,k,v所求出的环显然不是那个最小环。然而我们知道,这个最小环中必定有一个最大点kO,也就是说,虽然当前k没有求出我们所需要的最小环,但是当我们从k做到kO的时候,这个环上的所有点都小于kO了.也就是说在k=kO时一定能求出这个最小环。我们用一个实例来说明:假设最小环为1—3—4—5—6—2—1。的确,在u=l,v=4,k=3时,k<6,dist[3,4,1]的确求出的不是4—5—6—2—1这个环,但是,当u=4,v=6,k=5或u=5,v=2,k=6时,dist[k,v,u]表示的都是这条最短路径.所以我们在Floyd以后,只要枚举u.v,k三个变量即可求出最小环。时间复杂度为O(n3)。我们可以发现,Floyd和最后枚举u,v,k三个变量求最小环的过程都是u,v,k三个变量,所以我们可以将其合并。这样,我们在k变量变化的同时,也就是进行Floyd算法的同时,寻找最大点为k的最小环。


    讲的听清楚的。

    下面是我的板子(例题HDU1599):

    #include <bits/stdc++.h>
    using namespace std;
    #define IO ios::sync_with_stdio(false);cin.tie(0)
    #define forn(i, n) for(int i = 0; i < n; ++i)
    
    const int inf = 2e7;
    
    int dis[105][105], a[105][105];
    
    int main() {
        IO;
        int n, m;
        while(cin >> n >> m) {
            forn(i, 105) forn(j, 105) dis[i][j] = a[i][j] = inf;
            forn(i, m) {
                int u, v, w;
                cin >> u >> v >> w;
                dis[u][v] = min(w, dis[u][v]);
                a[u][v] = a[v][u] = dis[v][u] = dis[u][v];
            }
            int ans = inf;
            for(int k = 1; k <= n; ++k) {
                for(int i = 1; i < k; ++i) {
                    for(int j = i + 1; j < k; ++j) {
                        ans = min(ans, dis[i][j] + a[i][k] + a[k][j]);
                    }
                }
                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]);
                    }
                }
            } 
            if(ans != inf)  cout << ans << '
    ';
            else cout << "It's impossible." << '
    ';
        }
        return 0;
    }

    2. Johnson算法

    引用链接:点我

    Johnson 和 Floyd 一样,是一种能求出无负环图上任意两点间最短路径的算法。该算法在 1977 年由 Donald B. Johnson 提出。

    Part 1 算法概述

    任意两点间的最短路可以通过枚举起点,跑 nn 次 Bellman-Ford 算法解决,时间复杂度是 O(n^2m)O(n2m) 的,也可以直接用 Floyd 算法解决,时间复杂度为 O(n^3)O(n3) 。

    注意到堆优化的 Dijkstra 算法求单源最短路径的时间复杂度比 Bellman-Ford 更优,如果枚举起点,跑 nn 次 Dijkstra 算法,就可以在 O(nmlog m)O(nmlogm) (本文中的 Dijkstra 采用 priority_queue 实现,下同)的时间复杂度内解决本问题,比上述跑 nn 次 Bellman-Ford 算法的时间复杂度更优秀,在稀疏图上也比 Floyd 算法的时间复杂度更加优秀。

    但 Dijkstra 算法不能正确求解带负权边的最短路,因此我们需要对原图上的边进行预处理,确保所有边的边权均非负。

    一种容易想到的方法是给所有边的边权同时加上一个正数 xx ,从而让所有边的边权均非负。如果新图上起点到终点的最短路经过了 kk 条边,则将最短路减去 kxkx 即可得到实际最短路。

    但这样的方法是错误的。考虑下图:

    1 o 21→2 的最短路为 1 o 5 o 3 o 21→5→3→2,长度为 -2−2。

    但假如我们把每条边的边权加上 55 呢?

    新图上 1 o 21→2 的最短路为 1 o 4 o 21→4→2 ,已经不是实际的最短路了。

    Johnson 算法则通过另外一种方法来给每条边重新标注边权。

    我们新建一个虚拟节点(在这里我们就设它的编号为 00 )。从这个点向其他所有点连一条边权为 00 的边。

    接下来用 Bellman-Ford 算法求出从 00 号点到其他所有点的最短路,记为 h_ihi​ 。

    假如存在一条从 uu 点到 vv 点,边权为 ww 的边,则我们将该边的边权重新设置为 w+h_u-h_vw+hu​−hv​ 。

    接下来以每个点为起点,跑 nn 轮 Dijkstra 算法即可求出任意两点间的最短路了。

    容易看出,该算法的时间复杂度是 O(nmlog m)O(nmlogm) 。

    Q:那这么说,Dijkstra 也可以求出负权图(无负环)的单源最短路径了?
    A:没错。但是预处理要跑一遍 Bellman-Ford,还不如直接用 Bellman-Ford 呢。

    Part 2 正确性证明

    为什么这样重新标注边权的方式是正确的呢?

    在讨论这个问题之前,我们先讨论一个物理概念——势能。

    诸如重力势能,电势能这样的势能都有一个特点,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。

    势能还有一个特点,势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。

    接下来回到正题。

    在重新标记后的图上,从 ss 点到 tt 点的一条路径 s o p_1 o p_2 o dots o p_k o ts→p1​→p2​→⋯→pk​→t 的长度表达式如下:

    (w(s,p_1)+h_s-h_{p_1})+(w(p_1,p_2)+h_{p_1}-h_{p_2})+ dots +(w(p_k,t)+h_{p_k}-h_t)(w(s,p1​)+hs​−hp1​​)+(w(p1​,p2​)+hp1​​−hp2​​)+⋯+(w(pk​,t)+hpk​​−ht​)

    化简后得到:

    w(s,p_1)+w(p_1,p_2)+ dots +w(p_k,t)+h_s-h_tw(s,p1​)+w(p1​,p2​)+⋯+w(pk​,t)+hs​−ht​

    无论我们从 ss 到 tt 走的是哪一条路径, h_s-h_ths​−ht​ 的值是不变的,这正与势能的性质相吻合!

    为了方便,下面我们就把 h_ihi​ 称为 ii 点的势能。

    上面的新图中 s o ts→t 的最短路的长度表达式由两部分组成,前面的边权和为原图中 s o ts→t 的最短路,后面则是两点间的势能差。因为两点间势能的差为定值,因此原图上 s o ts→t 的最短路与新图上 s o ts→t 的最短路相对应。

    到这里我们的正确性证明已经解决了一半——我们证明了重新标注边权后图上的最短路径仍然是原来的最短路径。接下来我们需要证明新图中所有边的边权非负,因为在非负权图上,Dijkstra 算法能够保证得出正确的结果。

    根据三角形不等式,新图上任意一边 (u,v)(u,v) 上两点满足: h_v leq h_u + w(u,v)hv​≤hu​+w(u,v) 。这条边重新标记后的边权为 w'(u,v)=w(u,v)+h_u-h_v geq 0w′(u,v)=w(u,v)+hu​−hv​≥0 。这样我们证明了新图上的边权均非负。

    至此,我们就证明了 Johnson 算法的正确性。

    我的代码:(洛谷P5905)

    #include <bits/stdc++.h>
    using namespace std;
    #define ll long long
    #define forn(i, n) for(int i = 0; i < n; ++i)
    #define for1(i, n) for(int i = 1; i <= n; ++i)
    #define IO ios::sync_with_stdio(false);cin.tie(0)
    
    const int maxn = 3e3 + 5;
    const int inf = 1e9;
    
    int n, m;
    
    map<int,int> mp[maxn];
    vector<pair<int,int> >e[maxn], g[maxn];
    int dis[maxn], h[maxn], vis[maxn];
    bool inq[maxn];
    
    void spfa() {
        queue<int>q;
        for1(i, n) {
            h[i] = 0;
            inq[i] = 1;
            q.push(i);
        }
        while(!q.empty()) {
            int u = q.front(); q.pop();
            inq[u] = 0;
            for(auto x : e[u]) {
                int w = x.second, v = x.first;
                if(h[v] > h[u] + w) {
                    h[v] = h[u] + w;
                    
                    if(!inq[v]) {
                        q.push(v), inq[v] = 1;
                        ++vis[v];
                        if(vis[v] == n) {
                            cout << -1 << '
    ';
                            exit(0);
                        }
                    }
                }
            }
        }
        for1(u, n) {
            for(auto &x : e[u]) {
                int v = x.first, w = x.second;
                g[u].push_back({v, w + h[u] - h[v]});
            }
        }
    }
    
    void dij(int s) {
        for1(i, n) dis[i] = inf, vis[i] = 0;
        priority_queue<pair<int,int> >pq;
        pq.push({0, s});
        dis[s] = 0;
        while(!pq.empty()) {
            auto now = pq.top(); pq.pop();
            int u = now.second;
            if(vis[u]) continue;
            vis[u] = 1;
            for(auto &x : g[u]) {
                int v = x.first, w = x.second;
                if(vis[v]) continue;
                if(dis[v] > dis[u] + w) {
                    dis[v] = dis[u] + w;
                    pq.push({-dis[v],v});
                }
            }
        }
        ll ans = 0;
        for1(i, n) {
            if(dis[i] == inf) ans += 1ll * i * inf;
            else ans += 1ll * i * (dis[i] - h[s] + h[i]);
        }
       // if(s == 1)cerr<<'
    ';
        cout << ans << '
    ';
    }
    
    int main() {
        IO;
        forn(i, maxn) h[i] = inf;
        cin >> n >> m;
        forn(i, m) {
            int u, v, w; cin >> u >> v >> w;
            if(u == v) {
                if(w < 0) return cout << -1 << '
    ', 0;
                continue;
            }
            if(!mp[u].count(v)) mp[u][v] = w;
            else mp[u][v] = min(mp[u][v], w);
        }
        for1(i, n) {
            for(auto &x : mp[i]) {
                e[i].push_back({x.first, x.second});
            }
        }
        spfa();
        for1(i, n) dij(i);
        return 0;
    }

    3. 有三道还挺有意思的例题,我放在B站讲了:https://space.bilibili.com/255125226,有两道题搜不到一道题是bzoj4289

    4. 最短路计数:两道洛谷例题P1608、P1144还有一道NOIP2017(P3953)提高组最后一题,也是最短路计数。

    P1144题意:n个点m条边无向无权图,求1-n的最短路方案数。数据范围很小,但是可以O(n)做。

    我们知道最短路取一个点的dis时条件是dis[v] > dis[u] + w(u-v)。而有时候会有dis[v] == dis[u] + w(u-v),也就是这种条件会对种类数++,然后按照DP的思路往下走就OK,因为BFS和dij都是经过的点不会再次访问所以就没有任何问题。

    代码: 

    #include <bits/stdc++.h>
    using namespace std;
    #define IO ios::sync_with_stdio(false);cin.tie(0)
    #define forn(i, n) for(int i = 0; i < n; ++i)
    #define for1(i, n) for(int i = 1; i <= n; ++i)
    
    const int maxn = 1e6 + 5;
    const int mod = 100003;
    
    vector<int>e[maxn];
    bool vis[maxn];
    int dis[maxn], ans[maxn];
    
    int main() {
        IO;
        int n, m; cin >> n >> m;
        forn(i, m) {
            int u, v; cin >> u >> v;
            e[u].push_back(v);
            e[v].push_back(u);
        }
        queue<int>que; 
        que.push(1);
        dis[1] = 0, ans[1] = 1, vis[1] = 1;
        while(!que.empty()) {
            int u = que.front(); que.pop();
            for(auto v : e[u]) {
                if(!vis[v]) dis[v] = dis[u] + 1, ans[v] = ans[u], vis[v] = 1, que.push(v);
                else if(dis[v] == dis[u] + 1) (ans[v] += ans[u]) %= mod;
            }
        }
        for1(i, n) cout << ans[i] << '
    ';
        return 0;
    }

    P1608只是把图改成有向带权而已(也就是dij)

    #include <bits/stdc++.h>
    using namespace std;
    #define IO ios::sync_with_stdio(false);cin.tie(0)
    #define ll long long
    #define forn(i, n) for(int i = 0; i < n; ++i)
    #define for1(i, n) for(int i = 1; i <= n; ++i)
    
    const int inf = 2e9;
    const int maxn = 2e3 + 5;
    
    int g[maxn][maxn];
    vector<pair<int, int> >e[maxn];
    bool vis[maxn];
    int dis[maxn];
    ll ans[maxn];
    
    int main() {
        IO;
        //freopen("P1608_5.in", "r", stdin);
        int n, m; cin >> n >> m;
        forn(i, m) {
            int u, v, w; cin >> u >> v >> w;
            if(!g[u][v]) g[u][v] = w;
            else g[u][v] = min(g[u][v], w);
        }
        for1(u, n) {
            for1(v, n) {
                if(u == v) continue;
                if(!g[u][v]) continue;
                //cerr << "@#!  " << u << ' '<< v << ' '<< g[u][v] << '
    ';
                e[u].push_back({v, g[u][v]});
            }
        }
        priority_queue<pair<int, int> >pq; 
        pq.push({0, 1});
        for1(i, n) dis[i] = inf;
        dis[1] = 0, ans[1] = 1;
        while(!pq.empty()) {
            auto now = pq.top(); pq.pop();
            int u = now.second;
            if(vis[u]) continue;
            vis[u] = 1;
            for(auto &x : e[u]) {
                int v = x.first, w = x.second;
                if(dis[v] > dis[u] + w) {
                    dis[v] = dis[u] + w;
                    ans[v] = ans[u];
                    pq.push({-dis[v], v});
                }else if(dis[v] == dis[u] + w) ans[v] += ans[u];
            }
        }
        if(dis[n] == inf) cout << "No answer" << '
    ';
        else cout << dis[n] << ' ' << ans[n] << '
    ';
        return 0;
    }

    P3953就很有意思了,设1-n最短路长为X 题意要找的是1-n路径长度在[x, x + d]之间。

    洛谷很多博客说用topu序+dp搞,随便弄个样例就hack了,topu序的前提条件是DAG,只是洛谷的数据弱。但80%的博客都在讲

    用topu序就很离谱。剩下有10%说跑spfa,更离谱。spfa极限复杂度O(n*m).

    那么真正的做法是什么?

    这道题的d最大才50,那么我们开个Dp[maxn][50],dpij表示的是在点i比最短路多j的方案数,然后倒着跑一遍DP,在起点取ans就可以了。

    #include <bits/stdc++.h>
    using namespace std;
    #define IO ios::sync_with_stdio(false);cin.tie(0)
    #define ll long long
    #define forn(i, n) for(int i = 0; i < n; ++i)
    #define for1(i, n) for(int i = 1; i <= n; ++i)
    
    const int maxn = 1e5 + 5;
    const int maxm = 2e5 + 5;
    const int inf = 2e9;
    
    bool ok = 1;
    int n, m, k, mod, tot;
    
    struct edage {
        int nex, v, w;
    }e[maxm], g[maxm];
    int head[maxn], head2[maxn], dis[maxn];
    bool vis[maxn], viss[maxn][55], visss[maxn][55];
    ll dp[maxn][55];
    
    inline int dfs(int u, int val) {
        if(visss[u][val]) return dp[u][val];
        viss[u][val] = visss[u][val] = 1;
        for(int i = head2[u]; i; i = g[i].nex) {
            int v = g[i].v, w = g[i].w;
            int nval = val + dis[u] - dis[v] - w;
            //cerr << "!@#   "<< u << ' ' << v << ' ' << w << ' ' << nval << '
    '; 
            if(nval < 0) continue;
            if(viss[v][nval]) {
                ok = 0;
                viss[u][val] = 0;
                return 0;
            }
            (dp[u][val] += dfs(v, nval)) %= mod;
        }
        viss[u][val] = 0;
        return dp[u][val];
    }  
    
    inline void dij() {
        forn(i, n + 5) dis[i] = inf, vis[i] = 0;
        priority_queue<pair<int, int> >pq;
        pq.push({0, 1});
        dis[1] = 0;
        while(!pq.empty()) {
            auto now = pq.top(); pq.pop();
            int u = now.second;
            //cerr << u << ' ' << vis[u] << '
    '; 
            if(vis[u]) continue;
            vis[u] = 1;
            for(int i = head[u]; i; i = e[i].nex) {
                int v = e[i].v, w = e[i].w;
                //cerr << u << ' ' << v << ' '<< w << ' ' << dis[v] << ' '<< '
    ';
                if(dis[v] > dis[u] + w) {
                    dis[v] = dis[u] + w;
                    pq.push({-dis[v], v});
                }   
            }
        }
    }
    inline void add(int u, int v, int w) {
        e[++tot] = {head[u], v, w}, head[u] = tot;
        g[tot] = {head2[v], u, w}, head2[v] = tot;
    }
    inline void init() {
        tot = 0, ok = 1;
        forn(i, n + 5) {
            forn(j, k + 5) dp[i][j] = visss[i][j] = 0;
        }
        forn(i, m + 5) head[i] = head2[i] = 0; 
    }
    
    int main() {    
        IO;
        //freopen("park.in", "r", stdin);
        int T; cin >> T; while(T--) {
            cin >> n >> m >> k >> mod;
            //cerr << n << ' ' << m << ' ' << k << ' ' << mod << '
    ';
            init();
            forn(i, m) {
                int u, v, w;
                cin >> u >> v >> w;
                add(u, v, w); 
            }
            dij();
            ll ans = 0;
            add(n + 1, 1, 0);
            dp[n + 1][0] = 1, visss[n + 1][0] = 1, dis[n + 1] = 0;
            forn(i, k + 1) (ans += dfs(n, i)) %= mod;
            if(ok) cout << ans << '
    ';
            else cout << -1 << '
    '; 
        }
        return 0;
    }
  • 相关阅读:
    记一次开发的日常2020-01-09
    python configparser模块
    python logging模块
    python eval() hasattr() getattr() setattr() 函数使用方法详解
    redis 连接池
    Python 数据库连接池
    Object arrays cannot be loaded when allow_pickle=False
    注册网站 captcha reCHAPTCHA 错误
    网站收藏
    Python创建命令行应用的工具 tools for command line application in python
  • 原文地址:https://www.cnblogs.com/AlexPanda/p/12520275.html
Copyright © 2020-2023  润新知