zoukankan      html  css  js  c++  java
  • 2. 最短路问题

    1. 单源最短路问题 (Bellman-Ford 算法)  

    Bellman_Ford

      单源最短路是固定一个起点,求它到其他所有点的最短路问题。

      记从起点 s 出发到顶点 i 的最短距离为 d[ i ],则有等式成立:

          d[ i ] = min{ d[ j ] + (从 j 到 i 的边的权值) | e = (j, i) ∈ E }

      如果给定的图是一个DAG,就可以利用这条递推关系计算出 d。但是如果图中有圈,就无法这样计算。

      记当前到顶点 i 的最短路长度为 d[ i ], 并设初值 d[ s ] = 0, d[ i ] = INF, 再不断使用递推式更新 d 的值, 就可以算出新的 d。只要图中不存在负圈(总长度小于0的有向环路),这样的更新操作就是有限的。结束之后 d 就是所求最短距离。

    // 从顶点 from 指向顶点 to 的权值为 cost 的边
    struct edge{
        int from;
        int to;
        int cost;
    }; 
    edge es[MAX_E];
    int d[MAX_V];  //最短距离 
    int V, E;
    
    //求解从 s 出发到所有点的最短距离
    void shortest_path(int s) {
        for (int i = 0; i < V; i++)    d[i] = INF;
        d[s] = 0;
        while(true) {
            bool update = false;
            for (int i = 0; i < E; i++) {
                edge e = es[i];
                if (d[e.from ] != INF && d[e.to ] > d[e.from ] + e.cost ) {
                    d[e.to ] = d[e.from ] + e.cost;
                    update = true;
                }
            }
            if (!update) break;
        }
    } 

      如果在图中不存在从 s 可达的负圈, 那么最短路不会经过同一顶点两次(也就是说最多通过 | V | - 1 条边),while(true) 最多执行 | V | - 1 次,因此复杂度是0(| V | * | E |)。反之,如果存在从 s 可达的负圈,那么在第| V | 次循环中也会更新 d 的值, 因此可用这个性质来检查负圈。如果一开始对所有顶点 i, 都把 d[ i ] 初始化为0,那么可以检查出所有的负圈。

    // 如果返回 true 则存在负圈
    bool find_negative_loop() {
        memset(d, 0, sizeof(d));
        
        for (int i = 0; i < V; i++) {
            for (int j= 0; j < E; j++) {
                edge e = es[j];
                if(d[e.to] > d[e.from] + e.cost) {
                    d[e.to] = d[e.from] + e.cost;
                    // 如果第 n 次仍然更新了,则存在负圈
                    if (i == V - 1) return true; 
                }
            }
        }
        return false;
    } 

    SPFA(Shortest Path Faster Algorithm)

      这是基于Bellman-Ford 的思想,采用先进先出(FIFO)队列进行优化的一个计算单源最短路的快速算法。

      只要最短路径存在,SPFA算法必能求出最小值。我们假定最短路一定存在,即图中没有负权圈,所以每个结点都有最短路径值。每次入队的点的d【】值都在变小,在达到最短路径后,算法结束。

      如果最短路径不存在时,即存在负圈,并且起点可以到达负圈,那么利用SPFA会进入死循环,因为d【】值会越来越小,无限循环,使得算法无法退出。若不存在负圈,则任何最短路上的点必定小于等于 | V |,换言之,我们用 vis[ i ] 来记录这个点入队的次数,所有的 vis[ i ] <= | V |,如果vis[ i ] > | V |,则表明这个图存在负圈。

      复杂度:0(| V || E |)

    struct edge{
        int to;
        int cost;
    };
    vector<edge> G[MAX_V];
    int d[MAX_V];                //最短距离 
    int vis[MAX_V];                //节点i被访问的次数 
    bool inq[MAX_V];            //表示结点i是否在队列中
    
    bool SPFA(int s) {
        memset(d, INF, sizeof(d));
        memset(vis, 0, sizeof(vis));
        memset(inq, false, sizeof(inq));
        
        queue<int> q;
        q.push(s);
        d[s] = 0;
        inq[s] = true;
        
        while (!q.empty()) {
            int v = q.front();
            q.pop();
            inq[v] = false;
            
            if (vis[v]++ > V) return true;        //判断是否存在负圈,如果存在则返回true 
            
            for (int i = 0; i < G[v].size(); i++) {
                edge e = G[v][i];
                if (d[e.to] > d[v] + e.cost ) {
                    d[e.to] = d[v] + e.cost;
                    if (!inq[e.to ]) {
                        inq[e.to] = true;
                        q.push(e.to);
                    }
                }
            }    
        }
        return false;
    }

    这里参考自:

    夜深人静写算法(四)-差分约束  

    2.单源最短路问题(Dijkstra 算法)

      让我们来考虑一下没有负边的情况。在 Bellman-Ford算法中,如果d[ i ] 还不是最短距离的话,那么即使进行 d[ j ] = d[ i ] + (从 i 到 j 的边的权值的更新),d[ j ]也不会变成最短距离。而且即使d[ i ] 没有变化, 每次循环也要检查一遍从 i 出发的所有边。这显然是浪费时间的。因此对算法做如下修改。

      (1) 找到最短距离已经确定的顶点,从它出发更新相邻顶点的最短距离。

      (2) 此后不需要再关心(1)中的最短距离已经确定的点。

      那么怎么确定这个顶点?在最开始的时候,只有起点的最短距离是确定的,而在尚未使用的顶点中,距离 d[ i ] 最小的顶点就是最短距离已经确定的顶点。这是因为由于不存在负边,所以 d[ i ]不会在之后的更新中变小。

                                                               

    int cost[MAX_V][MAX_V]; //cost[u][v]表示边 e = (u,v)的权值,不存在是为INF
    int d[MAX_V];           //顶点s出发的最短距离
    bool used[MAX_V];       //已经使用过的图 
    int V;
    
    //求从起点s出发到各顶点的最短距离
    void dijkstra(int s) {
        fill(d, d + V, INF);
        fill(used, used + V, false);
        d[s] = 0;
        
        while(true) {
            int v = -1;
            //从尚未使用的顶点中选择一个距离最小的顶点
            for (int u = 0; u < V; u++) 
                if (!used[u] && (v == -1 || d[u] < d[v])) 
              v = u; if (v == -1) break; used[v] = true; for (int u = 0; u < V; u++) d[u] = min(d[u], d[v] + cost[v][u]); } }

      使用邻接矩阵实现的话复杂度为0(| V |2).使用邻接表的话,更新最短距离只需要访问每条边一次即可,因此这部分的复杂度为0(| E |)。但每次都要枚举所有的顶点来查找下一个使用的顶点,因此最终复杂度仍为0(| V |2)。需要优化的是数值的插入(更新)和取出,使用堆可以解决。把每个顶点当前的最短距离用堆来维护。而每次从堆中取出的最小值就是下一次要使用的顶点。这样堆中的元素共有0(| V |)个,更新和取出数值的操作有0(| E |)次,因此整个算法复杂度为0(| E | log | V |)。

    struct edge {
        int to;
        int cost;
    }; 
    typedef pair<int, int> P; //first 是最短距离, second是顶点的编号
    int V;
    vector<edge> G[MAX_V];
    int d[MAX_V];
    
    void dijkstra(int s) {
        //通过指定greater<P> 参数,堆按照first从小到大排序
        priority_queue<P, vector<P>, greater<P> > q;
        fill(d, d + V, INF);
        q.push(P(0, s));
      d[s] = 0;
    while (!q.empty()) { P p = q.top(); q.pop(); int v = p.second; if (d[v] < p.first) continue; for (int i = 0; i < G[v].size(); i++) { edge e = G[v][i]; if (d[e.to] > d[v] + e.cost) { d[e.to ] = d[v] + e.cost; q.push(P(d[e.to], e.to)); } } } }

      相对于Bell-Ford的0(| V | | E |)的复杂度,Dijkstra算法的复杂度是0(| E | log | V |)更高效,但是在图中存在负边的情况下,Dijkstra就无法正确求解,还是需要用Bell-Ford算法。

      这里可能会对SPFA和dijkstra+heap优化产生混淆,提供一篇非常清晰的辨别博客:

      SPFA和dijkstra+heap区别

    eg :POJ-2387

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<vector>
    #include<cstring>
    #include<queue>
    using namespace std;
    const int MAX_V = 10101;
    int INF = 0x3f3f3f3f;
    struct edge {
        int to;
        int cost;
    }; 
    typedef pair<int, int> P; //first 是最短距离, second是顶点的编号
    int V;
    vector<edge> G[MAX_V];
    int d[MAX_V];
    
    int T, N;
    
    void dijkstra(int s) {
        //通过指定greater<P> 参数,堆按照first从小到大排序
        priority_queue<P, vector<P>, greater<P> > q;
        fill(d, d + V, INF);
        q.push(P(0, s));
        d[s] = 0;
        while (!q.empty()) {
            P p = q.top();
            q.pop();
            int v = p.second;
            if (d[v] < p.first) continue;
            for (int i = 0; i < G[v].size(); i++) {
                edge e = G[v][i];
                if (d[e.to] > d[v] + e.cost) {
                    d[e.to ] = d[v] + e.cost;
                    q.push(P(d[e.to], e.to));
                }
            }
        } 
    }
    int main() {
        cin >> T >> N;
        V = N;
        for (int i = 0; i < T; i++) {
            int p;
            edge e, ee;
            cin >> p >> e.to >> e.cost ;
            ee.to = p - 1;
            e.to -= 1;
            ee.cost = e.cost;
            G[p - 1].push_back(e);
            G[e.to].push_back(ee);
        }
        //cout << endl;
        /*for (int i = 0; i < N; i++) {
            for (int j = 0; j < G[i].size(); j++)
                cout << i << ' ' << G[i][j].to << ' ' << G[i][j].cost << endl;
        }*/
        dijkstra(0);
        //for (int i = 0; i < N; i++)
        cout << d[N - 1] << endl;
        return 0;
    }
    View Code

     POJ-3268

    这题大意是给你有向图,求每个点到 X 的最短路径 + X 到每个点的最短路径(原路径都不能使用),求出需要的最长时间

    思路:第一遍用dijkstra 算法, 第二次只要把图反转即 cost[ i ][ j ] = cost[ j ][ i ],再使用dijkstra一次即可。当然 每个点到X的最短路也就是X到每个点的最短路(估计就我一个人不这么想) 

    #include<iostream>
    #include<cstring>
    using namespace std;
    int cost[1010][1010];
    int d[1010];
    bool vis[1010];
    int ans[1010];
    int V, E, X;
    int INF = 0x3f3f3f3f;
    
    void dijkstra(int s) {
        fill (d, d + V, INF);
        fill (vis, vis + V, false);
        d[s] = 0;
        
        while (true) {
            int v = -1;
            for (int i = 0; i < V; i++)
                if (!vis[i] && (v == -1 || d[i] < d[v]))
                    v = i;
            if (v == -1) break;
            vis[v] = true;
            for (int i = 0; i < V; i++)
                d[i] = min(d[i], d[v] + cost[v][i]);
        }
        
        for (int i = 0; i < V; i++)
            ans[i] += d[i];
        
            
    } 
    
    int main() {
        cin >> V >> E >> X;
        //fill(cost[0], cost[0] + V * V, INF);
        memset(cost, INF, sizeof(cost));
        for (int i = 0; i < E; i++) {
            int a, b, c;
            cin >> a >> b >> c;
            cost[a - 1][b - 1] = c;
        }
        dijkstra(X - 1);
        for (int i = 0; i < V; i++)
            for (int j = i; j < V; j++)  // 这里我写成了 for(int j = 0; j < V)..这样会又改回来 
                swap(cost[i][j], cost[j][i]);
        dijkstra(X - 1);
        int maxs = 0;
        for (int i = 0; i < V; i++)
            maxs = max(maxs, ans[i]);
        cout << maxs << endl;
        
    }
    View Code

     POJ-2066

    这题就是把家到附近城市的距离设置为0, 一遍Dijkstra就可以了 。但是这些题最坑的是  居然有重边。

    因为草儿的家在一个小镇上,没有火车经过,所以她只能去邻近的城市坐火车(好可怜啊~)。
    Input
    输入数据有多组,每组的第一行是三个整数T,S和D,表示有T条路,和草儿家相邻的城市的有S个,草儿想去的地方有D个; 
    接着有T行,每行有三个整数a,b,time,表示a,b城市之间的车程是time小时;(1=<(a,b)<=1000;a,b 之间可能有多条路) 
    接着的第T+1行有S个数,表示和草儿家相连的城市; 
    接着的第T+2行有D个数,表示草儿想去地方。
    Output
    输出草儿能去某个喜欢的城市的最短时间。
    Sample Input
    6 2 3
    1 3 5
    1 4 7
    2 8 12
    3 8 4
    4 9 12
    9 10 2
    1 2
    8 9 10
    Sample Output
    9
    View Code
    #include<iostream>
    #include<vector>
    #include<queue>
    #include<algorithm>
    #include<cstdio>
    #include<utility>
    #include<functional>
    #include<cstring>
    using namespace std;
    typedef pair<int, int> P;
    const int MAX_V = 1005;
    int INF = 0x3f3f3f3f;
    int map[MAX_V][MAX_V];
    int d[MAX_V];
    int s[MAX_V], f[MAX_V];
    bool used[MAX_V];
    int T, S, D;
    void dijkstra(int s) {
        memset(d, INF, sizeof(d));
        memset(used, false, sizeof(used));
        //priority_queue<P, vector<P>, greater<P> > q;
        //q.push(P(0, s));
        d[s] = 0;
        /*while (!q.empty()) {
            P p = q.top(); q.pop();
            int v = p.second;
            if (d[v] < p.first) continue;
            for (int i = 0; i < MAX_V; i++) {
                if(map[v][i] == -1) continue;
                if (d[i] > d[v] + map[v][i]) {
                    d[i] = d[v] + map[v][i];
                    q.push(P(d[i], i));
                }
            }
        }*/
        while(true) {
            int v = -1;
            for (int i = 0; i < MAX_V; i++)
                if (!used[i] && (v == -1 || d[i] < d[v])) v = i;
            if (v == -1) break;
            used[v] = true;
            for (int i = 0; i < MAX_V; i++)
                d[i] = min(d[i], d[v] + map[v][i]);
        }    
    }
    
    int main() {
        while (~scanf("%d%d%d", &T, &S, &D)) {
            memset(map, INF, sizeof(map));
            for(int i = 0; i < MAX_V; i++)
                map[i][i] = 0;
            //memset(d, INF, sizeof(d));
            for (int i = 0; i < T; i++) {
                int a, b, c;
                scanf("%d%d%d", &a, &b, &c);
                if (c < map[a][b])
                    map[a][b] = map[b][a] = c; 
            }
            for (int i = 0; i < S; i++) {
                scanf("%d", &s[i]);
                map[0][s[i]] = 0;
                map[s[i]][0] = 0;
            }
            dijkstra(0);
            for (int i = 0; i < D; i++)
                scanf("%d", &f[i]);
            int mins = INF;
            for (int i = 0; i < D; i++)
                mins = min(mins, d[f[i]]);
            printf("%d
    ", mins);
        }    
    }
    View Code

    3.任意两点间的最短路问题(Floyd-Warshall 算法)

       试用DP来解决。 记  d[ k + 1 ][ i ][ j ] 为在只使用 0 ~ k 个顶点下从 i 到 j 的最短路径长度。 当 k = -1 时我们认为只使用了 i 和 j 两个顶点,

    所以d[ 0 ][ i ][ j ] = cost [ i ][ j ].  接下来思考只使用顶点 0 ~ k 的问题归约到只使用 0 ~ k - 1 的问题上。

      只使用 0 ~ k  时,我们 i 到 j 的最短路分两种情况:

        (1)正好经过顶点 k 一次 。 d[ k ][ i ][ j ] = d[ k - 1 ][ i ][ k ] + d[ k - 1 ][ k ][ j ].

        (2)完全不经过顶点 k 。 d [ k ][ i ][ j ] = d[ k - 1 ][ i ][ j ].

      所以递推式为(如果使用同一个数组更新):

         d[ i ][ j ] = min( d[ i ][ j ], d[ i ][ k ] + d[ k ][ j ] )

       复杂度为0(| V |3),Floyd-Warshall 算法和 Bellman-Ford 算法一样,可以处理边是负数的情况。而判断图中是否有负圈,只需检查是否存在 d[ i ][ j ]是负数的顶点 i 就可以了。

    int d[MAX_V][MAX_V];
    int V;
    
    void floyd_warshall() {
        for (int k = 0; k < V; k++)
            for (int i = 0; i < V; i++)
                for ( int j = 0; j < V; j++)
                    d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    }

      如果复杂度在可接受的范围内,单源最短路径也可以用 Floyd-Warshall 进行求解。

    4. 路径还原

      以 Dijkstra 算法为例,试还原最短路径。 在求解最短路径时,满足 d[ j ] = d[ k ] + cost[ k ][ j ] 的顶点 k,就是最短路上的前驱节点,因此通过不断寻找前驱节点就可以恢复出最短路。复杂度为 0(| E |)。

      此外,如果用 prev[ j ] 来记录最短路上顶点 j 的前驱,那么就可以在 0(| V |)的时间内完成最短路的恢复。 d[ j ] 被 d[ j ] = d[ k ] + cost[ k ][ j ] 更新时,修改 prev[ j ] = k,这样就可以得到 prev数组。在计算从 s 出发到 j 的最短路时,通过 prev[ j ] 就可以最短顶点 j 的前驱,因此不断把 j 替换成 prev[ j ]直到 j = s 为止就可以了。 其他两个算法类似。

    int prev[MAX_V];
    
    // 求从起点 s 出发到各个顶点的最短距离
    void dijkstra(int s) {
        fill(d, d + V, INF);
        fill(used, used + V, false);
        fill(prev, prev + V, -1);
        d[s] = 0;
        
        while(true) {
            int v = -1;
            for (int u = 0; u < V; u++)
                if (!used[u] && (v == -1 || d[u] < d[v]))
                    v = u;
            if (v == -1) break;
            used[v] = true;
            for (int u = 0; u < V; u++)
                if (d[u] > d[v] + cost[V][u]) {
                    d[u] = d[v] + cost[v][u];
                    pre[u] = v;
                }
        }
        
        //到顶点 t 的最短路
        vector<int> get_path(int t) {
            vector<int> path;
            for ( ; t != -1; t = prev[t])
            // 不断沿着 prev[t] 走,直到 t = s 
                path.push_back(t);   
            // 翻转就是 
            reverse(path.begin(), path.end());
            return path;
        }
    }

     5. 例题

       

      Dijkstra 算法的思路是依次确定尚未确定的顶点中距离最小的顶点,那么按照这个思路对算法进行少许修改就可以求出次短路。

      到某个顶点 v 的次短路要么是到其它某个顶点 u 的最短路再加上 u -> v 的边, 要么就是到 u 的次短路再加上 u -> v 的边,因此所需要求的就是到所以顶点的最短路和次短路。因此我们不仅要记录最短距离,还要记录次短距离。接下来只要用与 Dijkstra 算法相同的做法,不断更新这两个距离就可以求出次短路。

    typedef pair<int, int> P;
    struct edge{
        int to;
        int cost;
    };
    int N, R;
    vector<edge> G[MAX_N];
    int dis[MAX_N];        // 最短距离 
    int dis2[MAX_N];        // 次短距离 
    
    void solve() {
        priority_queue<P, vector<P>, greater<P> > q;
        fill(dis, dis + N, INF);
        fill(dis2, dis2 + N; INF);
        dis[0] = 0;
        q.push(P(0, 0));
        while (!q.empty()) {
            P p = q.top();
            q.pop();
            int v = p.second;
            int d = p.first ;
            if (dis2[v] < d) continue;
            for (int i = 0; i < G[v].size(); i++) {
                edge &e = G[v][i];
                int d2 = d + e.cost;
                if (dis[e.to] > d2) {
                    swap(dis[e.to], d2);
                    q.push(P(dis[e.to], d2));
                }
                if (dis2[e.to] > d2 && dis[e.to] < d2) {
                    dis2[e.to] = d2;
                    q.push(P(dis2[e.to], e.to));
                }
            }
        }
        printf("%d
    ", dis2[N - 1]);
    }
    View Code

      

  • 相关阅读:
    agc015D A or...or B Problem
    agc016E Poor Turkeys
    CTSC2016时空旅行
    假期的宿舍[ZJOI2009]
    上白泽慧音(luogu P1726
    小K的农场(luogu P1993
    Cashier Employment(poj1275
    Intervals(poj1201
    序列分割[Apio2014]
    特别行动队[APIO2010]
  • 原文地址:https://www.cnblogs.com/astonc/p/10739103.html
Copyright © 2011-2022 走看看