圖論 - 2

By 王政祺

還記得「圖」嗎?

來試試以下這些名詞吧

無向圖、連通圖

路徑、簡單路徑

鄰接矩陣、鄰接串列

生成樹

最短路徑

給定一張\(V\)個點、\(E\)條邊的圖

試求 \((x, y)\) 兩點之間經過最少條邊的路徑

但要是邊有權重呢?

前提: 不能有負環!!!

單點源最短路徑

鬆弛(relaxation)

對於任意兩個點 \(u, v\),如果起點到他們的距離分別為 \(d_u, d_v\) 且滿足 \(d_u+w_{u,v} < d_v\),那我們就可以把 \(d_v\) 更新為 \(d_u+w_{u,v}\)

Bellman-Ford Algorithm

 \(O(E)\) 枚舉邊來進行鬆弛,鬆弛 \(V-1\) 次

最短路徑長度最大\(v-1\),若鬆弛\(v-1\)次後還能再鬆弛就表示存在負環

複雜度 \(O(VE)\)

tf = 0; // tf 表示是否有負環
dis[1] = 0;
for (int i = 2; i <= N; i++) dis[i] = INF; // 起點到 i 的最短距離,初始設為無限大
for (int i = 1; i < N; i++) { // 跑 v-1 次
	for (int j = 0; j < M; j++) { // 窮舉邊
		a = path[j].ff, b = path[j].ss;
		if (dis[a] != INF && dis[a] + c[j] < dis[b]) { // 如果可以 relax 的話
			dis[b] = dis[a] + c[j];
		}
	}
}
for (int i = 0; i < M; i++) {
	a = path[i].ff, b = path[i].ss;
	if (dis[a] + c[i] < dis[b]) { // 如果還能 relax 的話就表示有負環
		tf = 1;
	}
}

利用Bellman-Ford實作負環判斷

好像有點慢(?

來一點優化吧

SPFA

每次只枚舉有被鬆弛到的點的邊

期望複雜度 \(O(V+E)\),最差複雜度 \(O(VE)\)

能不能再快一點???

如果沒有負邊的話...?

最短路徑樹

子節點的最短路徑在沒有負邊的情況下一定不比父節點小

Dijkstra’s Algorithm

每次選一個不在樹上且距離最近的點

加進樹中並用它來鬆弛其他點

用 priority_queue 維護!

複雜度 \(O((E+V)\log E)\)

vector < pii > path[N+1]; // adjacency list
priority_queue < pii , vector < pii >, greater < pii > > pq;
pii cur;
fill(dis, dis+N, INF);
dis[1] = 0;
pq.push({0, 1});
//每 個 節 點 只 會 被 更 新 一 次
for(int i = 0; i < N; i++){
//將 已 更 新 的 節 點 從 清 單 移 除(Lazy Deletion)
	do cur = pq.top(), pq.pop();
	while(cur.ff > dis[cur.ss]);
	for(auto e : path[cur.ss]) // Relax
		if(dis[e.ss] > cur.ff+e.ff)
			dis[e.ss] = cur.ff+e.ff,
			pq.push({dis[e.ss], e.ss});
}

Dijkstra

全點對最短路徑

用 Dijkstra 的話需要 \(O(V*(V+E) log E)\) 

感覺有點糟QQ

一個路徑除了起點終點

剩下的都是中繼點

有沒有一種 DP 的感覺

Floyd-Warshall Algorithm

把「由 \(i\) 點中途經過前 \(k\) 點抵達 \(j\) 點的最短路徑」作為 DP 狀態

 \(dp_{i,j,k}= min(dp_{i,j,k-1}, dp_{i,k,k-1}+dp_{k,j,k-1})\)

for (int i = 1; i <= n; i++) {
	for (int j = 1; j <= n; j++) {
    	dis[i][j] = INF; // 初始化
        if(i==j) dis[i][j] = 0;
    }
}

//cin

for (int k = 1; k <= n; k++)// 窮舉 k 來看看能不能 Relax dis(i, j)
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			if (dis[i][k] + dis[k][j] < dis[i][j])
				dis[i][j] = dis[i][k] + dis[k][j];

Floyd-Warshall 實作

最小生成樹

一張有邊權的連通無向圖

求一個邊權總和最小的生成樹

一個點 = 自己的最小生成樹

有點玄?但還算可以理解嗎(?

兩個最小生成樹要怎麼合併

加上連接兩者之中最小的那條邊就對了

三個呢?

反正不管怎樣挑最短的那條來連就對了

Kruskal’s algorithm

每次找當前邊權最小的邊,檢查他的兩邊是否在同一個集合(生成樹)中

不在的話,就把它連起來吧

所以說有人記得曾經有個叫做並查集的東西嗎?

Disjoint Sets

int dsu[MAXN+5];
int qry(int now) {
	if (dsu[now] != now) dsu[now] = qry(dsu[now]);
	return dsu[now];
}
void Union(int a, int b) {
	dsu[qry(a)] = qry(b);
}

Kruskal's Algorithm

sort(path, path+m, cmp); // 讓 path 陣列依照邊權值大小排序
for (int i = 0; i < m; i++) {
	if (qry(path[i].a) == qry(path[i].b)) continue;
	ans += path[k].c; // 將答案加上權重
	Union(path[k].a, path[k].b);
}

Prim’s algorithm

其實就是類似 dijkstra 啦

最短路徑樹 ~ 最小生成樹

把記錄的當前最短路徑改為「與當前最小生成樹的距離」

資訊讀書會 圖論-2

By CasperWang

資訊讀書會 圖論-2

  • 949