1. 程式人生 > 其它 >AcWing 345 牛站 【BellmanFord演算法,非正解】

AcWing 345 牛站 【BellmanFord演算法,非正解】

題目傳送門

  • \(Bellman-Ford\)演算法求解經過不超過\(k\)條邊的最短路

點這裡

  • \(Bellman-Ford\)演算法求解經過恰好\(k\)條邊的最短路(可以重複經過)

前人的足跡,推薦閱讀

一、解法I

#pragma GCC optimize(2) //累了就吸點氧吧~

#include <bits/stdc++.h>
using namespace std;
int id[1010];
const int N = 210;
const int M = 1000010;
int d[N], backup[N];
struct Edge {
    int a, b, c;
} edges[M];
int n, k, m, S, E;

int bellmanFord() {
    memset(d, 0x3f, sizeof(d));
    d[S] = 0;
    for (int i = 0; i < k; i++) {
        memcpy(backup, d, sizeof(d));
        //與最多經過k條邊這裡不同!
        memset(d, 0x3f, sizeof(d));
        for (int j = 0; j < m; j++) {
            int a = edges[j].a, b = edges[j].b, c = edges[j].c;
            //與最多經過k條邊這裡不同!
            d[b] = min(d[b], backup[a] + c);
            d[a] = min(d[a], backup[b] + c);
        }
    }
    return d[E];
}
int main() {
    cin >> k >> m >> S >> E;
    //開始節點為1號
    if (!id[S]) id[S] = ++n;
    //結束結點為2號,為了防止起點和終點是一個,採用了判斷的辦法
    if (!id[E]) id[E] = ++n;
    //修改原來S和E的離散化後的值
    S = id[S], E = id[E];
    // m條邊
    for (int i = 0; i < m; i++) {
        int a, b, c;
        cin >> c >> a >> b;
        if (!id[a]) id[a] = ++n;
        if (!id[b]) id[b] = ++n;
        edges[i].a = id[a], edges[i].b = id[b], edges[i].c = c;
    }
    //上面的離散化簡直太牛B了!
    printf("%d", bellmanFord());
    return 0;
}

二、解法II

#pragma GCC optimize(2) //累了就吸點氧吧~

#include <bits/stdc++.h>
using namespace std;
const int N = 205, M = 105;
//現在看來,這個程式碼寫的好垃圾啊,離散化用的不好。程式碼長
//而且,在用舊的號碼記錄到鄰接表中,然後再去修改,邏輯太長,垃圾!
//相對於另外兩套程式碼,都是在使用hashTable或者unordered_map進行對映後
//再放入鄰接表(鄰接矩陣),就方便多了~
int m;    //圖的邊數
int k;    //恰好是k條邊
int s, t; //起點和終點

struct Edge {
    int a; //起點
    int b; //終點
    int w; //邊長
} edges[M];

int p[N]; //使用了哪些點,為了照顧後面的離散化,肯定要儘可能的把點都儲存起來,
int tot;
int dist[N], backup[N];

//通過STL將原來的x找到現在新的下標位置
int get(int x) {
    return lower_bound(p, p + tot, x) - p;
}

int bellmanFord() {
    //最多經過 k 條邊:在迴圈外初始化一次
    //恰好經過 k 條邊:除在迴圈外初始化一次外
    //還需要在第一層迴圈中memcpy(backup)後面再次初始化
    memset(dist, 0x3f, sizeof dist);
    dist[s] = 0; //出發點

    for (int i = 0; i < k; i++) {
        // 恰好是k條邊,迭代k次
        /*
        1、memset可以理解為是陣列有多大,就執行多少次(實際效能比這個強,可以這樣估算)
        2、其實就算不離散化,直接上來就BellmanFord也是可以的,就是會TLE
        3、為什麼會TLE呢?因為n最大是1000,點數最大是1e6,那麼將面對1000*1e6=1e9的時長
        ,C++1秒過不了。
        4、觀察到點數雖多,但邊數少,只有100條。滿打滿算邊兩頭的端點都不一樣的話,最多
        也就200個點。
        5、也就是說很多點沒有邊連著,是沒用的,可以考慮去掉這些無用的點,保留有邊的點。
        6、去的辦法就是離散化,列舉邊,找出邊的兩個端點,然後對端點集合進行離散化。
        7、離散化後,需要修改原來點與點之間的關聯關係,比如原來是1000,2000這兩個點之間
        有一條長度為100的邊,現在1000對映成了1號點,2000對映成了2號點,需要修改為1到2有
        一條長度為100的邊才對。
        8、我們需對點進行離散化,所以需要把點放到一個數組中,在本程式碼中就是p陣列,
        point的意思。這裡使用的儲存圖的方式為按點+邊儲存方式,所有點儲存在p陣列中,
        所有邊儲存在e陣列中,每一條邊包含三個資訊:起點,終點,長度。
        這樣儲存的辦法,使得我們可以離散化掉無用的點。

       BellmanFord演算法的存圖方式比較牛X(yxc大佬原話),用一個結構體Edge(u,v,w),
       然後開一個數組就行了,隨便存,與之前學習的鄰接表不同。
       原因很簡單:因為BellmanFord需要遍歷每一條邊,怎麼能快速遍歷每一條邊就是關鍵。
       我們以前學習過的鄰接表,是按點做索引的,然後記錄這個點出發到達哪些點。
       如果採用鄰接表,就需要列舉每個點,然後二次迴圈找出每條邊,就麻煩了,不如直接
       以邊為索引來的快了。

        9、本題還有一個重要問題:bellmon-ford 演算法解決的問題是最多經過k條邊的最短路,
        而題目問的是恰好經過k條邊的最短路徑,怎麼解決呢?
        答:bellmon-ford 每次強制轉移即可,即把新的陣列賦值為正無窮。
        這樣每迭代一次陣列中i代表的一定是恰好走k步的步數。
        這麼做的原理是什麼呢?什麼地方講過這個原理呢?是現想出來的嗎?不可能吧~
        */
        // 每次在處理dist陣列前備份一下到backup陣列
        // 只用上一次的迭代結果,就不會發生串聯了。
        // 上一次的迭代結果可以理解為在上一次x條邊限定的情況下的意思
        // https://www.acwing.com/video/285/
        // 第18分鐘講到了關於串聯的問題
        memcpy(backup, dist, sizeof dist);
        memset(dist, 0x3f, sizeof dist);
        for (int j = 1; 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);
            dist[a] = min(dist[a], backup[b] + w);
        }
    }
    //計算在n條限定下的最短距離
    return dist[t];
}

int main() {
    cin >> k >> m >> s >> t;
    for (int i = 1; i <= m; i++) {
        int a, b, w;
        //一條邊的邊長以及構成邊的兩個點的編號
        cin >> w >> a >> b;
        edges[i] = {a, b, w};
        //記錄有哪些點,準備離散化去重
        p[tot++] = a, p[tot++] = b;
    }
    //離散化
    sort(p, p + tot);
    tot = unique(p, p + tot) - p;
    //列舉每條邊,將第i條邊的起點和終點轉化為離散化後的新序號
    for (int i = 1; i <= m; i++)
        edges[i].a = get(edges[i].a), edges[i].b = get(edges[i].b);
    //將起點和終點也轉化為離散化後的新點號
    s = get(s), t = get(t);
    //呼叫bellmanFord演算法
    printf("%d\n", bellmanFord());
    return 0;
}