学习笔记-最短路

觉得讲的不详细可以去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 << '\n';
        else cout << "It's impossible." << '\n';
    }
    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(nm\log m)O(nmlogm) (本文中的 Dijkstra 采用 priority_queue 实现,下同)的时间复杂度内解决本问题,比上述跑 nn 次 Bellman-Ford 算法的时间复杂度更优秀,在稀疏图上也比 Floyd 算法的时间复杂度更加优秀。

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

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

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

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

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

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

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

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

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

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

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

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

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

Part 2 正确性证明

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

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

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

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

接下来回到正题。

在重新标记后的图上,从 ss 点到 tt 点的一条路径 s \to p_1 \to p_2 \to \dots \to p_k \to 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 \to ts→t 的最短路的长度表达式由两部分组成,前面的边权和为原图中 s \to ts→t 的最短路,后面则是两点间的势能差。因为两点间势能的差为定值,因此原图上 s \to ts→t 的最短路与新图上 s \to 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 << '\n';
                        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<<'\n';
    cout << ans << '\n';
}

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 << '\n', 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] << '\n';
    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] << '\n';
            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" << '\n';
    else cout << dis[n] << ' ' << ans[n] << '\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 << '\n'; 
        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] << '\n'; 
        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] << ' '<< '\n';
            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 << '\n';
        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 << '\n';
        else cout << -1 << '\n'; 
    }
    return 0;
}

 

posted @ 2020-01-28 06:28  AlexPanda  阅读(204)  评论(0编辑  收藏  举报