1. 程式人生 > 其它 >基礎之最短路

基礎之最短路

關於我知識點全忘了需要從頭來過這件事

最短路

Floyed

DP的角度。

從節點 \(\large A\) 到節點 \(\large B\) 最短路徑只有兩種情況,要麼直接從 \(\large A\)\(\large B\) ,要麼經過若干個點再到 \(\large B\)

\(\large dis(A,B)\) 為從節點 \(\large A\) 到節點 \(\large B\) 的最短路徑長,那麼列舉 \(\large A,B\) 間斷點 \(\large K\) ,若有 \(\large dis(A,K)+dis(K,B) < dis(A,B)\) ,那麼更新 \(\large dis(A,B)\)

。遍歷完所有的斷點 \(\large K\) 後,\(\large dis(A,B)\) 就是我們要的答案。

由這個思路直接得出的程式碼:

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
        for (int k = 1; k <= n; k++) {
            (dis[i][k] + dis[k][j] < dis[i][j]) and (dis[i][j] = dis[i][k] + dis[k][j]);
        }
    }
}

然而,上面的程式碼是錯誤的,這裡要注意迴圈的巢狀順序, \(\large K\)

放最裡面是錯的。

因為這樣的列舉方式會過早地將 \(\large i,j\) 間的最短路徑確定下來,後面存在更短的路徑時,就會無法更新。

有些抽象??舉個例子

如果將 \(\large K\) 放在最內層,那麼 \(\large A->B\) 只能更新一條路徑,即 \(\large A->B\) (列舉其他兩個點時,與 \(\large A\)\(\large B\) 的連邊邊權為 \(\large INF\)),但很顯然是錯誤的。把 \(\large K\) 放在中間也是一樣的道理,這裡不再細說。

那麼把 \(\large K\) 放在最外層呢?當我們列舉到斷點 \(\large C\)

時,會更新 \(\large B->D\)\(\large B->C->D\) ,這樣接下來列舉到斷點 \(\large D\) ,又會更新 \(\large A->B\) ,答案正確。

for (int k = 1; k <= n; k++) {
    for (int i = 1; i <= n; i++) {
         for (int j = 1; j <= n; j++){
            (dis[i][k] + dis[k][j] < dis[i][j]) and (dis[i][j] = dis[i][k] + dis[k][j]);
        }
    }
}

但是這玩意兒 \(\large O(n^3)\) 的,誰閒著沒事寫這玩意兒

路徑的儲存自己探索吧,我懶得打了。。。

Dijkstra

額。。。上面用的是 \(\large DP\) ,這邊用的是貪心。

用的範圍挺廣的,有必要好好總結一下。

演算法特點:

單源最短路,即可解決固定一點到其他任意點的最短路徑問題。最終得到的是一個最短路徑樹。

他往往,是其他圖論演算法的子模組。

演算法策略:

\(\large Dijkstra\) 採用貪心策略,宣告 陣列 \(\large dis\) 儲存源點到各個頂點的最短路徑長度 和 一個儲存已經找到了最短路徑的頂點集合 \(\large T\)

下面設源點為 \(\large s\) .

初始狀態,\(\large dis[s]=0\) 。查找出邊(我習慣用連結串列存圖,雖然某些情況下有些慢),將與其直接相連的頂點 \(\large m\) 間的距離設為此邊邊權,即 \(\large dis[m]=w_{s\rightarrow m}\) ,同時把與 \(\large s\) 不直接相連的點間的距離設為無窮大。

初始時,集合 \(\large T\) 內只有 \(\large s\) ,然後,從 \(\large dis\) 中選出最小值,則該值就是當前源點 \(\large s\) 到該值對應的頂點的最短路徑,將該點加入 \(\large T\) 中。

然後,看看新加入的頂點是否可以到達其他頂點並且看看通過這個新加的頂點後,到達其他頂點的路徑是否更優,是,那就替換。

重複上述操作,直到 \(\large T\) 中包含所有點。

\(\large ps\) :一開始選最小值選出來的為什麼就是最短路徑?(課下思考

但他無法解決帶有負邊權的圖。

畢竟只有在確定當前達到最短情況下才會將頂點加入 \(\large T\) ,但很顯然,這玩意兒太貪了,他會在一個負邊上反覆橫跳。。。。

code:

上程式碼(帶堆優化

#include <bits/stdc++.h>

#define _ 0
#define N 100010
#define int long long
//防爆好習慣

using namespace std;

template <typename T>
inline void read (T &a) {
	T x = 0, f = 1;
	char ch = getchar ();
	while (! isdigit (ch)) {
		(ch == '-') and (f = 0);
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ '0');
		ch = getchar ();
	}
	a = f ? x : -x;
}

struct blanc {
    int to, w, net;
} e[N << 1]; // 在無向圖的情況下,邊數 >=n <=2n
int tot, head[N];

inline void add (int u, int v, int w) {
    e[++tot].to = v;
    e[tot].w = w;
    e[tot].net = head[u];
    head[u] = tot;
}

priority_queue <pair <int, int>, vector <pair <int, int> >, greater <pair <int, int> > > q;
bool vis[N];
int dis[N << 1];

inline void dij () {
    while (! q.empty ()) {
        int y = q.top ().second;
        q.pop ();
        if (vis[y]) continue ;
        vis[y] = 1;
        for (int i = head[y]; i; i = e[i].net) {
            int v = e[i].to;
            if (dis[v] > dis[y] + e[i].w) {
                dis[v] = dis[y] + e[i].w;
                q.push (make_pair (dis[v], v));
            }
        }
    }
}

int n, m, u, v, w;
int start;

signed main () {
    read (n), read (m);
    read (start);
    
    for (int i = 1; i <= n; i++) {
        dis[i] = 2147483647;
    }
    
    for (int i = 1; i <= m; i++) {
        read (u), read (v), read (w);
        add (u, v, w);
        //add (v, u, w);
    }
    dis[start] = 0;
    q.push (make_pair (0, start));
    dij ();
    for (int i = 1; i <= n; i++) {
        printf ("%lld\n", dis[i]);
    }
    return ~~(0^_^0);
}

沒了(這就是你說的好好總結??

SPFA

秉著公平公正的原則讓這玩意兒出來詐詐屍

然鵝你也可以看出來我的這部分寫得極不負責。。。

我更喜歡叫它 SPA

可以直接跳過!

演算法簡介:

\(\large SPFA\ \ \ (Shortest\ Path\ Faster\ Algorithm)\) 演算法,是一種死掉的演算法(bushi,是一種單源最短路演算法,是對 \(\large Bellman-ford\) 的佇列優化。正常情況下挺快的,但只是正常。。。

這玩意兒能處理負邊權(\(\large Dij\) 表示羨慕。複雜度大約是 \(\large O(kE)\)\(\large k\) 為每個點的平均入隊次數,稀疏圖中小於 \(\large 2\) 。但在稠密圖中,這玩意兒複雜度是。。。。 \(\large O(過不了)\) 。。。。

演算法實現:

建一個佇列,初始時佇列只有起點,再建立一個表格記錄起點到所有點的最短路徑(就跟 \(\large Dij\) 一樣。然後執行鬆弛操作(術語去死,也跟 \(\large Dij\) 那個貪法差不多。然後沒了。

但他其實是通過佇列的收斂性得到答案的。

還可以判負環,即一個點入隊次數超過 \(\large N\)

演算法具體化:

就是給圖舉例子

十分經典的一個圖(照著別人的重畫一遍

\(\large A\rightarrow E\) 的最短路

下面不畫圖了(畫圖實在太難了

但我可以偷圖啊

源點入隊

擴充套件與 \(\large A\) 相連的邊, \(\large B,C\) 入隊

\(\large B,C\) 再擴充套件,\(\large D\) 入隊

下面操作自己描述

\(\large E\) 出隊,佇列為空,演算法結束

code:

#include <bits/stdc++.h>

#define N 100010
#define _ 0

using namespace std;

template <typename T>
inline void read (T &a) {
	T x = 0, f = 1;
	char ch = getchar ();
	while (! isdigit (ch)) {
		(ch == '-') and (f = 0);
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ '0');
		ch = getchar ();
	}
	a = f ? x : -x;
}

struct blanc {
    int to, net, w;
} e[N << 1];
int head[N], tot;

inline void add (int u, int v, int w) {
    e[++tot].to = v;
    e[tot].w = w;
    e[tot].net = head[u];
    head[u] = tot;
}

int dis[N], in[N], n, m; // in 存某點入隊次數,判負環
bool vis[N];

inline bool spfa (int s) {
    memset (dis, 0x3f, sizeof dis);
    int u, v;
    queue <int> q;
    q.push (s);
    vis[s] = 1;
    dis[s] = 0;
    while (! q.empty ()) {
        u = q.front ();
        q.pop ();
        vis[u] = 0;
        for (int i = head[u]; i; i = e[i].net) {
            v = e[i].to;
            if (dis[v] > dis[u] + e[i].w) {
                dis[v] = dis[u] + e[i].w;
                if (! vis[v]) {
                    q.push (v);
                    vis[v] = 1;
                    if (++ in[v] > n) return 0;
                }
            }
        }
    }
    return 1;
}

int s, x, y, z;

signed main () {
    read (n), read (m), read (s), read (ed);
    for (int i = 1; i <= m; i++) {
        read (x), read (y), read (z);
        add (x, y, z);
        //add (y, x, z);
    }
    if (! spfa (s)) {
        puts ("FALSE!");
    } else {
        for (int i = 1; i <= n; i++) {
            printf ("%d ", dis[i]);
        }
    }
    return ~~(0^_^0);
}

這個交到 luogu 上會 T 掉......

還沒完。。。