停課競賽第三天2020/10/29

HHT0618發表於2020-10-29

最短路:

一、樸素版dijkstra

合適的使用範圍:無負權邊的稠密圖
演算法複雜度: O ( n 2 ) O(n^2) O(n2)

實現方式:按點更新,用當前最近的沒有更新到的點更新其他沒更新到的點。第一層列舉 n n n次,第二層1判斷最近的點,第二層2更新其他還沒更新到的點。
程式碼實現:

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[j] < dist[t])) t = j;
        st[t] = true;
        for (int j = 1; j <= n; j++)
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
} 

如果有負權邊,則可能使得最短路比原來的長。舉例:
1 − 2 − 4 , 1 − 3 − 5 , 2 − 3 − ( − 3 ) 1-2-4,1-3-5,2-3-(-3) 12413523(3)。可以發現 1 1 1 3 3 3 的最短路為 1 1 1 ,但由於dijkstra的貪心思想,會在還沒走 2 − 3 2-3 23 這條邊之前就將1-2的路徑更新了。

二、堆優化版dijkstra

合適的使用範圍:無負權邊的稀疏圖
演算法複雜度: O ( m l o g m ) O(mlogm) O(mlogm)

實現方式:與樸素版dijkstra相似,但是,是用最短邊去更新沒有更新到的點。第一層列舉最短邊,第二層更新沒有更新到的點。
程式碼實現:

struct node {
    int x, y;
    bool operator < (const node &a) const {
        return y > a.y;
    }
};
int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[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++;
}
int dijkstra() {
    memset(dist, 0x3f, sizeof(dist));
    priority_queue<node> q;
    dist[1] = 0;
    q.push({1, 0});
    while (q.size()) {
        node t = q.top();
        q.pop();
        if (st[t.x]) continue;
        for (int i = h[t.x]; i != -1; i = ne[i]) {
            if (dist[e[i]] > dist[t.x] + w[i]) {
                dist[e[i]] = dist[t.x] + w[i];
                q.push({e[i], dist[e[i]]});
            }
        }
        st[t.x] = true;
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

同樣如果有負權邊,則無法判斷是否存在最短路。

三、Bellman-ford

合適的使用範圍:有負權邊且有最短路邊數限制的圖(可以判斷負環)
演算法複雜度: O ( n m ) O(nm) O(nm)

實現方式:第一次列舉 n n n次,第二次列舉 m m m條邊,可以證明n次迭代後,如果有最短路必能求出,列舉k次即k條邊的最短路。
注意:當用該演算法求k條邊的最短路時要有備份backup[n],原因在於在一次 n n n 的列舉中不能用已經更新過的點去更新其他點。最後判斷有無最短路時要用已得距離與一個較大的數進行比較。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, M = 10010;

struct Edge {
    int a, b, c;
} edge[M];

int n, m, k;
int dist[N], backup[N];

int Bellman_ford() {
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    for (int i = 0; i < k; i++) {
        memcpy(backup, dist, sizeof(dist));
        for (int j = 0; j < m; j++) 
            if (dist[edge[j].b] > backup[edge[j].a] + edge[j].c)
                dist[edge[j].b] = backup[edge[j].a] + edge[j].c;
    }
    
    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    else return dist[n];
}

int main() {
    scanf("%d %d %d", &n, &m, &k);
    for (int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        edge[i].a = a, edge[i].b = b, edge[i].c = c;
    }
    if (Bellman_ford() == -1) printf("impossible\n");
    else printf("%d\n", dist[n]);
    return 0;
}

四、SPFA

合適的適用範圍:有負權邊且沒最短路邊數的限制的圖(可以判斷負環)
演算法複雜度:一般 O ( m ) O(m) O(m),最壞 O ( n m ) O(nm) O(nm)

實現方式:與Bellman_ford演算法相似,不同之處在於要用一個佇列來儲存改變了值的點,節點的值沒變的則不列舉。所以,該演算法也與堆優化的dijkstra演算法十分相似。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

int n, m;
int h[N], e[N], w[N], ne[N], idx;
int dist[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++;
}

int spfa() {
    memset(dist, 0x3f, sizeof(dist));
    dist[1] = 0;
    queue<int> q;
    q.push(1);
    st[1] = true;
    while (q.size()) {
        int 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];
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return dist[n];
}

int main() {
    memset(h, -1, sizeof(h));
    scanf("%d %d", &n, &m);
    for (int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
    }
    if (spfa() == 0x3f3f3f3f) printf("impossible\n");
    else printf("%d\n", dist[n]);
    return 0;
}

五、Floyd

演算法實現:動態規劃,列舉中間點,中間點首先列舉,可以有負權邊,但不能有負環。
例題

給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環,邊權可能為負數。

再給定k個詢問,每個詢問包含兩個整數x和y,表示查詢從點x到點y的最短距離,如果路徑不存在,則輸出“impossible”。

資料保證圖中不存在負權迴路。

#include <cstring>
#include <iostream>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q;
int d[N][N];
void floyd() {
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main(){
    scanf("%d%d%d", &n, &m, &Q);
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;
    while (m -- ) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);
    }
    floyd();
    while (Q -- ) {
        int a, b;
        scanf("%d%d", &a, &b);
        int t = d[a][b];
        if (t > INF / 2) puts("impossible");
        else printf("%d\n", t);
    }
    return 0;
}

六、SPFA判斷負環

實現方式:根據SPFA的實現原理我們可以知道當所有的點都無法更新的時候演算法就會結束了,但如果有負環的話則會導致程式無法結束。根據抽屜原理我們可以知道,一個有 n n n 個節點的圖,如果沒出現環的話,那麼從一個點到另一個的最短路的最多經過 n − 1 n - 1 n1個點,所以我們可以根據轉移數量來判定一個圖中有沒有負環。

#include <cstdio>
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool st[N];
int cnt[N];
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int spfa() {
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        st[i] = true;
        q.push(i);
    }
    while (q.size()) {
        int 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]) {
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }
    return false;
}
int main() {
    memset(h, -1, sizeof(h));
    scanf("%d %d", &n, &m);
    for (int i = 0; i < m; i++) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
    }
    if (spfa()) printf("Yes\n");
    else printf("No\n");
    return 0;
}

七、小結

        今天的內容看起來很少,但是含量很足,每種演算法都是求最短路的,但它們之前的使用範圍和複雜度都是不同的,之後還要多刷題鞏固。

相關文章