圖:BFS、DFS、CycleDetection、UnionFind、Kruskal、Prim、TopoSort、Kosaraju、Tarjan、Dijkstra、BF、Floyd、Johnson

阿西,CSDN插入圖片實在太頭疼了,以後直接貼github地址吧

零、基本概念

圖的概念

頂點(Vertex),邊(Edges),度(degree): 入度和出度indegree,outdegree

有向無環圖:DAG

握手定理(無向圖):一個聚會上,把每個人握手的次數相加必爲偶數,也就是所有頂點的度加起來等於邊數的兩倍。

圖的分類

  • 有向圖(Directed Graph):單箭頭,頂點A,B: (A,B) != (B,A)

  • 無向圖(Un-Directed Graph):雙箭頭,頂點A,B: (A,B) = (B,A)

圖的表示:

圖可以用鄰接矩陣或者鄰接鏈表表示。鄰接矩陣適用於稠密圖的情況,鄰接鏈表適用於稀疏圖的情況。

  • 鄰接矩陣(adjacency matrix):

    ​ 圖有V個頂點,則設置 V x V 0-1矩陣,若矩陣元素A[i,j]=1,則有邊連接Vi,Vj

    ​ 在無向圖中,矩陣對稱。若將0,1設爲邊的權重,則爲加權圖。

    優點:插入、刪除、查找邊 O(1),缺點:佔用空間,增加頂點O(V^2)

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YnYUYPPi-1583824845583)(圖\1.png)]

  • 鄰接列表(adjacency list)

    鏈表組成的數組,數組大小等於頂點數。鏈表記錄一個頂點的所有鄰接點。

    優點:佔用空間等於O(V+E),增加頂點容易。缺點:查詢邊O(V)

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JVhWSCvy-1583824845591)(圖\2.png)]

一、圖的遍歷

1.1 廣度優先搜索(BFS)

Breadth First Search:思路:Traverse nodes in layers,類似樹的層序遍歷。

問題在於,若圖循環,按層遍歷會多次訪問同一結點。解決之道:用bool標記。

調用的結構:queue,一個結點出去,其所有還未入隊過的相鄰結點入隊。

注意:如果圖不是連通的!那麼! 要對所有結點進行一次是否被訪問的檢測。

**例上圖:**0爲起點;0出,13入;1出,256入;3出,4入;2出;5出;6出;4出。如果上圖另外還有一個7-8,非連通,則從7開始進行第二輪BFS。

1.2 深度優先搜索(DFS)

Depth First Search:思路:類似樹的先序遍歷。從某個頂點出發,只要有選擇,就不斷往前走,要是沒路了,就退回,直到棧爲空。對訪問過的節點用bool標記。

注意:如果圖不是連通的!那麼!要對所有結點進行一次是否被訪問的檢測。

調用結構:stack,用棧記錄走過的路,便於退回。

**例上圖:**1爲起點;訪問3,1入棧;訪問4,3入棧;訪問6,4入棧;6無路退回,4出棧;訪問2,4入棧;訪問5,2入棧;5無路退回,2出棧;2無路,4出棧;4無路退回,3出棧,訪問0,3入棧;0無路退回,3出棧;3出棧,3無路,1出棧,1無路,結束。如果上圖另外還有一個7-8,非連通,則從7開始進行第二輪BFS。

DFS中不同邊的分類

邊的分類是在對圖進行DFS纔有的概念,同一張圖中,DFS的方式不同,產生的邊的類型也不一樣。在普通的DFS中,用來記錄結點是否被訪問只需要用到2個值,也就是bool visited[V]。

在擴展應用中,被訪問過的結點會有3個值:char visited[V]

  • -1:表示結點未被訪問
  • 0:表示結點被訪問,但後代沒被訪問完,也就是還在棧中,還在這條路上沒返回
  • 1:表示結點被訪問完,後代也被訪問完,已經從這條路上返回了

邊的分類:DFS中,對於一條邊 u -> v

  • forward edges : visited[v]=1,從祖先指向其子輩的邊。
  • back edges:visited[v]=0,v已經被訪問完,還在這條路上,v->u->v,成環!
  • cross edges:visited[v]=1, v已經被訪問,後代也被訪問,uv沒有祖孫關係,是兄弟或者更遠甚至不在一棵樹上
  • tree edges: visited[v]=-1,v是首次被發現。DFS森林實際組成部分。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-54RmIRY9-1583824845593)(圖\0_0.jpg)]

1.3 複雜度考慮

每個節點僅遍歷一次,因此時間複雜度至少爲O(V)

除此之外,任何其他的複雜性都來自於如何發現每個節點的所有傳出路徑或邊,而這些又依賴於實現圖形的方式。典型的DFS實現使用哈希表維護遍歷的節點列表,以便您可以確定在O(1)時間(恆定時間)之前是否遇到過節點。

  • 如果將圖形實現爲鄰接矩陣(V x V數組),則對於每個節點,必須遍歷矩陣中長度爲V的整行以發現其所有出站邊。請注意,鄰接矩陣中的每一行都對應圖中的一個節點,並且該行存儲有關源自該節點的邊的信息。因此,複雜度爲O(V * V)= O(V ^ 2)

  • 如果圖是使用鄰接表實現的,其中每個節點都維護着其所有相鄰邊的列表,那麼對於每個節點,可以通過在線性時間內僅遍歷其鄰接表來發現其所有鄰居。對於有向圖,所有節點的鄰接表大小的總和爲E(邊的總數)。因此,DFS的複雜度爲O(V)+ O(E)= O(V + E)

    • 對於無向圖,每個邊將出現兩次。一旦在邊緣任一端的鄰接表中。因此,總體複雜度將爲O(V)+ O(2E)〜O(V + E)
  • 還有其他實現圖的方法。可以據此推斷複雜性。

1.4 代碼實現

Code : BFS&DFS
#pragma once
#include <iostream>
#include <list>
#include <queue>
#include <stack>
#include <vector>
using namespace std;
class AdjlistGraph
{
	int V;
	vector<list<int>> adj;
public:
	AdjlistGraph(int _V) :V(_V) {
		for (int i = 0; i < _V; i++)
		{
			list<int> ls;
			ls.push_back(i);
			adj.push_back(ls);
		}
	}
	void addEdge(int a, int b);
	void BFS();
	void DFS();
	void BFS(int start, vector<bool> &visited);
	void DFS(int start,vector<bool> &visited);
};

void AdjlistGraph::addEdge(int a, int b) {
	adj[a].push_back(b);
	adj[b].push_back(a);
}
// BFS遍歷
void AdjlistGraph::BFS() {
	vector<bool> visited(V, false);
	for (int i = 0; i < V; i++)
	{
		if (visited[i] != true) { 
			BFS(i, visited); cout << endl;
		}
	}
}
// 對圖的一個極大連通區域進行遍歷
void AdjlistGraph::BFS(int start, vector<bool> &visited) {
	queue<int> q;
	q.push(start);
	visited[start] = true;
	while (!q.empty())
	{
		int k = q.front();
		q.pop();
		cout << k << " ";
		for (auto i = adj[k].begin(); i!=adj[k].end(); i++){
			if (!visited[(*i)]) {
				q.push(*i);
				visited[(*i)] = true;
			}
		}
	}
}

// DFS遍歷
void AdjlistGraph::DFS() {
	vector<bool> visited(V, false);
	for (int i = 0; i < V; i++)
	{
		if (visited[i] != true) {
			DFS(i, visited); cout << endl;
		}
	}
}
// 對圖的一個極大連通區域進行遍歷
void AdjlistGraph::DFS(int start,vector<bool> &visited) {
	stack<int> s;
	s.push(start);
	cout << start << " ";
	visited[start] = true;
	while (!s.empty())
	{
		int v = s.top();
		auto i = adj[v].begin();
		bool flag = false;
		for (; i != adj[v].end(); i++){ 
			// 去找鏈表中的未訪問結點,找到就退出
			if (!visited[(*i)])
			{
				flag = true;
				break;
			}
		}
		if (flag == true)
		{
			s.push(*i);
			cout << (*i) << " ";
			visited[(*i)] = true;
		}
		else s.pop();
	}
}
Test : main.cpp
#include "AdjlistGraph.h"
int main(int argc, char ** argv) {
	AdjlistGraph g(7);
	g.addEdge(0, 1);
	g.addEdge(0, 3);
	g.addEdge(3, 1);
	g.addEdge(3, 4);
	g.addEdge(2, 5);
	g.addEdge(1, 5);
	g.addEdge(1, 6);
	g.addEdge(4, 6);
	g.addEdge(2, 4);
	g.addEdge(7, 8);
	g.BFS();
	cout <<endl << "************" << endl;
	g.DFS();
	cout <<endl<<  "************" << endl;

	system("pause");
	return 0;
}

1.5 應用:環檢測、拓撲排序、尋找強連通分量

self-loop:一個結點指向自己,自己成環

parallel edges: 兩個相鄰結點之間存在多條路徑

1.5.1 無向圖環檢測

BFS無向圖環檢測

BFS中有一個queue,用一個數組表示所有結點的狀態,-1表示未遇到,0表示遇到了,入了queue但沒訪問,1表示遇到了,出了queue並訪問了。當進行BFS遍歷的時候,需要將當前節點的鄰接點都放入queue中,若這些鄰接點中存在0,則說明有環存在。

DFS無向圖環檢測

通過DFS,判斷當前節點v,若有一v的相鄰頂點u已被訪問,且u不是v的父,則有環。解釋:DFS是沿着路徑走,對於無向圖,可以直接訪問其父,但是其他祖宗無環的話就無法訪問了。需要結構:visited數組(二元即可); 一變量:記錄父節點,若是需要記錄哪裏成環,用數組記錄pair(vertex,parent)即可。

Code:iscyclicDSFinUndirected
// DFS檢查無向圖一個極大連通區域的環,有環返回真
bool AdjlistGraph::iscyclicDSF() {
	vector<bool> visited(V, false);
	for (int i = 0; i < V; i++)
	{
		if (visited[i] != true) {
			if (iscyclicDSF(i, visited)) return true;
		}
	}
	return false;
}

// 對無向圖的一個極大連通區域進行遍歷
bool AdjlistGraph::iscyclicDSF(int start,vector<bool> &visited) {
	vector<int> s; // 因爲需要遍歷棧中的元素,所以用vector來代替棧的使用
	vector<int> parent(V,-1);// 令頭結點的父元素等於-1
	s.push_back(start);
	visited[start] = true;
	while (!s.empty()) // 此處也被修改
	{
		int v = s.back();
		bool flag = false;
		for (auto i = ++adj[v].begin(); i != adj[v].end(); i++)
		{	// 加個for循環來遍歷鄰接節點,看是否已被遍歷且非當前節點的父節點
			if (visited[(*i)] && parent[v]!=(*i) && parent[(*i)]!=v) {
				cout << (*i) << endl;
				return true;
			}
		}
		auto i = ++adj[v].begin();
		for (; i != adj[v].end(); i++){ 
			if (!visited[(*i)]) { 
				flag = true;
				break;
			}
		}
		if (flag == true){
			s.push_back(*i);
			visited[(*i)] = true;
			parent[(*i)] = v;
		}else s.pop_back();
	}
	return false;
}
Test:iscyclicDSFinUndirected
#include "AdjlistGraph.h"
int main(int argc, char ** argv) {
	AdjlistGraph g(8);
	g.addEdge(0, 1);
	g.addEdge(1, 2);
	g.addEdge(3, 2);
	g.addEdge(5,1);
	g.addEdge(5, 4);
	g.addEdge(2, 4);

	bool  res  = g.iscyclicDSF();
	if (res) {
		cout <<  "there is a cycle in the graph" << endl;
	}
	else {
		cout << "there is no cycle in the graph" << endl;
	}

	system("pause");
	return 0;
}

1.5.2 有向圖環檢測

用有向圖完成優先級問題,若有環,必無解。

DFS有向圖環檢測

通過DFS,來判斷當前節點v是否連接Stack中的祖先節點。若點v連接祖先節點w,則成環。對於非聯通的圖,多套一層函數對子圖做一次好啦,和DFS的處理是一樣的。

解釋:根據DFS,當前的路徑是從w->v,若v有邊指向祖先節點,則v->w,成環

具體實現:在DFS內部加了一層for循環遍歷stack判斷祖先。

測試用圖:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jPP3PmxI-1583824845598)(圖\4.png)]

Code : iscyclicDSFinDirectedGraph
#pragma once
#include <iostream>
#include <algorithm>
#include <list>
#include <vector>
using namespace std;
class AdjlistGraph
{
	int V;
	vector<list<int>> adj;
public:
	AdjlistGraph(int _V) :V(_V) {
		for (int i = 0; i < _V; i++)
		{
			list<int> ls;
			ls.push_back(i);
			adj.push_back(ls);
		}
	}
	void addEdge(int a, int b);
	
	bool iscyclicDSF();
	bool iscyclicDSF(int start,vector<bool> &visited);
};

void AdjlistGraph::addEdge(int a, int b) {
	adj[a].push_back(b);
}


// DFS檢查有向圖一個極大連通區域的環,有環返回真
bool AdjlistGraph::iscyclicDSF() {
	vector<bool> visited(V, false);
	for (int i = 0; i < V; i++)
	{
		if (visited[i] != true) {
			if (iscyclicDSF(i, visited)) return true;
		}
	}
	return false;
}

// 對有向圖的一個極大連通區域進行遍歷
bool AdjlistGraph::iscyclicDSF(int start,vector<bool> &visited) {
	vector<int> s; // 因爲需要遍歷棧中的元素,所以用vector來代替棧的使用
	s.push_back(start);
	visited[start] = true;
	while (!s.empty())
	{
		int v = s.back();
		bool flag = false;
		for (auto i = ++adj[v].begin(); i != adj[v].end(); i++)
		{	// 判斷其鄰接點是否在棧中,是則有環,返回退出
			// 其實鄰接表是順序訪問的,這個循環可以和下面的循環放在一起,
			//  但是爲了思路好看,就提出來了
			if (std::find(s.begin(), s.end(), *i) != s.end()) {
				cout << (*i) << endl;
				return true;
			}
		}
		auto i = ++adj[v].begin();
		for (; i != adj[v].end(); i++){ 
			if (!visited[(*i)]) { // 去找鏈表中的未訪問結點,有就壓入棧中
				flag = true;
				break;
			}
		}
		if (flag == true){
			s.push_back(*i);
			visited[(*i)] = true;
		}
		else s.pop_back();
	}
	return false;
}
Test : iscyclicDSFinDirectedGraph
#include "AdjlistGraph.h"
int main(int argc, char ** argv) {
	AdjlistGraph g(7);
	g.addEdge(0, 1);
	g.addEdge(0, 2);
	g.addEdge(1, 2);
	g.addEdge(3, 1);
	g.addEdge(3, 4);
	g.addEdge(4, 5);
	g.addEdge(5, 6);
	g.addEdge(6, 4);

	bool  res  = g.iscyclicDSF();
	if (res) {
		cout <<  "there is a cycle in the graph" << endl;
	}
	else {
		cout << "there is no cycle in the graph" << endl;
	}

	system("pause");
	return 0;
}

二、並查集UnionFind/Disjoint-set

Union-Find algorithm:Union-Find用於處理一些不交集的合併及查詢問題。可以用於解決許多經典的劃分問題,比如門派分類,城際連通,網絡連接等。

2.1 實現原理

兩步主要操作:

Find:找到當前元素的root,也就是確定元素屬於哪一個子集。元素進行一次查找後就將其祖先元素設爲根結點(路徑壓縮以優化查找速度)。

Union:將兩個子集合併成一個子集。合併時,高樹吸收矮樹(根據高度合併以優化查找速度)。

Find和Union中的兩步優化,確定兩個元素是否屬於同一子集的算法平攤時間是O(1),而不是O(n)。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hrUap7oU-1583824845604)(圖\6.png)]

2.1 代碼實現

Code:DisjointSet

此處並查集設置了兩個數組,一個數組用來記錄結點的父節點,另一個數組用來記錄樹的高度;可以簡化,parent數組初始化爲-1而不是自身,每次merge之後,都改變根結點的高度,用負數的大小來記錄樹的高度,其他的父節點任然用正常父節點表示。

#pragma once
class DisjointSet {
public:
	int V;
	int *parent; // parent[i]是i結點的父節點
	int *rank;   // rank[i]是樹高

    DisjointSet(int v) :V(v) {
		rank = new int[V];
		parent = new int[V];
		for (int i = 0; i < V; i++)
			parent[i] = i;
	}
	int find(int i);          // 查找到i的root
	void merge(int i, int j); // 合併倆集合
};

int DisjointSet::find(int i) {
	if (parent[i] == i) return i;
	else {
		parent[i] = find(parent[i]);
		return parent[i];
	}
}

void DisjointSet::merge(int i, int j) {
	int iset = find(i);
	int jset = find(j);
	if (iset == jset) return;

	int irank = rank[iset];
	int jrank = rank[jset];
	if (irank == jrank) {
		parent[iset] = j;
		rank[jset]++;
	}
	else irank < jrank ? parent[iset] = j : parent[jset] = i;
}
Test : DisjointSet
#include<iostream>
#include "DisjointSet.h"
using namespace std;
int main(int argc,char ** argv) {
	DisjointSet obj(5);
	obj.merge(0, 2);
	obj.merge(4, 2);
	obj.merge(3, 1);

	if (obj.find(4) == obj.find(0))
		cout << "4 0 in the same set" << endl;
	else
		cout << "4 0 in the different sets" << endl;
	
	if (obj.find(1) == obj.find(0))
		cout << "1 0 in the same set" << endl;
	else
		cout << "1 0 in the different sets" << endl;
}

2.3 應用:無向圖環檢測、Kruskal

Code : isCylicUsingDisjointSet
#pragma once
#include<vector>
class DisjointSet {
public:
	int V;
	int *parent; // parent[i]是i結點的父節點
	int *rank;   // rank[i]是樹高

    DisjointSet(int v) :V(v) {
		rank = new int[V];
		parent = new int[V];
		for (int i = 0; i < V; i++)
			parent[i] = i;
	}
	int find(int i);          // 查找到i的root
	void merge(int i, int j); // 合併倆集合
};

int DisjointSet::find(int i) {
	if (parent[i] == i) return i;
	else {
		parent[i] = find(parent[i]);
		return parent[i];
	}
}

void DisjointSet::merge(int i, int j) {
	int iset = find(i);
	int jset = find(j);
	if (iset == jset) return;

	int irank = rank[iset];
	int jrank = rank[jset];
	if (irank == jrank) {
		parent[iset] = j;
		rank[jset]++;
	}
	else irank < jrank ? parent[iset] = j : parent[jset] = i;
}

class Edge {
public:
	int src;
	int dst;
	int weight;
	Edge():src(0),dst(0),weight(0) {}
	Edge(int s,int d,int w):src(s),dst(d),weight(w) {}
};

class Graph {
public:
	int V, E;//頂點和邊數
	std::vector<Edge> edges;
	Graph(int v):V(v) {}
	Graph():V(0) {}
	void addEdge(Edge e);
	bool isCylic();
};
void Graph::addEdge(Edge e) {
	edges.push_back(e);
	E++;
}

bool Graph::isCylic(){
	DisjointSet subset(V);
	for (int i = 0; i < E; i++)
	{
		int x = subset.find(edges[i].src);
		int y = subset.find(edges[i].dst);
		if (x == y) return true;
		else
		{
			subset.merge(x, y);
		}
	}
	return false;
}
Test : isCylicUsingDisjointSet
#include<iostream>
#include"DisjointSet.h"
using namespace std;

int main(int argc,char ** argv) {
	Graph g(4);
	Edge e(0, 1, 1),f(0, 2, 1),c(1, 3, 1), h(2, 3, 1);
	g.addEdge(e);
	g.addEdge(f);
	g.addEdge(c);
	g.addEdge(h);
	
	if (g.isCylic()) cout << "true" << endl;
	else cout << "false" << endl;
	system("pause");
	return 0;
}

三、最小生成樹(Minimum Spanning Tree)

無向圖的生成樹是圖的一個子集。一個圖可以有多個生成樹。

3.1 基本概念

最小生成樹MST的特點:

  • 生成樹:無環,且連接所有頂點 。因此,有N個Vertices和N-1個Edges
  • 最小:所有邊的權值相加 = 權值和。不同生成樹的權值和不同。最小生成樹的權值和最小

最小生成樹的性質:

  • 從MST中移除一條邊,就不連通了
  • 從MST中增加一條邊,就成環了
  • 如果每條邊的權重不同,MST唯一
  • 完全無向連通圖可以有N^(N-2)種生成樹(ST)
  • 從完全連通圖中刪掉(邊數-頂點數+1)條邊,就有了生成樹(ST)
  • 無向圖中ST數量計算公式:從no.E中選擇no.V-1,然後減掉子環數量。----

最小生成樹的尋找有兩種算法,Kruskal算法,Prim算法。這兩種算法都基於貪心算法,也就是在每次選擇的時候選擇權值最小的邊。Kruskal是直接選擇權值最小的邊,而Prim算法是從頂點出發,間接選擇與頂點相連最小的邊。

3.2 Kruskal算法實現MST

步驟:

  • 若圖中有結點自己成環,刪掉;若圖中兩相鄰結點有多條邊,只保留最小邊。
  • 將圖中所有的邊都放到列表中,並根據權值從小到大排序
  • 選擇最小的邊,將邊放回到結點中,每放回一條邊都需判斷是否成環,是則丟棄,否則OK
  • 直到邊數到達N-1,結束

示例:原圖來自

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-x8hhoOo6-1583824845607)(圖\3_1.png)]

Code:KruskalMST
#pragma once
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

class Edge {
public:
	int weight;
	int src;
	int dst;
	Edge():weight(0),src(0),dst(0) {}
	Edge(int s,int d,int w ):weight(w),src(s),dst(d){}
};

class Graph
{
public:
	int V, E;//邊數和頂點數
	std::vector<Edge> edges;

	Graph() :V(0), E(0) {}
	Graph(int v):V(v) {}
	void addEdge(Edge e);
	void KruskalMST();
};

void Graph::addEdge(Edge e) {
	edges.push_back(e);
	E++;
}

class DisjointSet {
public:
	int V;
	int *parent;
	int *rank;

	DisjointSet(int v) :V(v) {
		rank = new int[V];
		parent = new int[V];
		for (int i = 0; i < V; i++)
			parent[i] = i;
	}

	int find(int i);
	void merge(int i, int j);
};

int DisjointSet::find(int i) {
	if (parent[i] == i) return i;
	else {
		parent[i] = find(parent[i]);
		return parent[i];
	}
}

void DisjointSet::merge(int i, int j) {
	int iset = find(i);
	int jset = find(j);
	if (iset == jset) return;

	int irank = rank[iset];
	int jrank = rank[jset];
	if (irank==jrank)
	{
		parent[iset] = j;
		rank[jset]++;
	}
	else irank < jrank ? parent[iset] = j : parent[jset] = i;
}

int myCompare(Edge a, Edge b) {
	return a.weight < b.weight;
}

void Graph::KruskalMST() {
	vector<Edge> mstedges; // 存儲mst的邊
	
	// 1.對所有的邊進行排序,從大到小排序
	sort(edges.begin(), edges.end(), myCompare);
	DisjointSet djs(V); // 創建並查集
	
	int i = 0; // 用來控制循環上限爲所有邊的數量
	int j = 0; // 用來控制循環下線爲MST的邊數量V-1
	while (j<V-1 && i<E )
	{
		// 2. 不斷選擇權值最小的邊
		Edge edge = edges[i++];
		int x = djs.find(edge.src);
		int y = djs.find(edge.dst);
		if (x != y) {
			mstedges.push_back(edge);
			j++;
			djs.merge(x, y);
		}
		// 若屬於同一集合則丟掉這條邊
	}
	cout << "MST tree edges :*******************" << endl;
	for (auto it = mstedges.begin();it!=mstedges.end(); it++)
	{
		cout << (*it).src << "---" << (*it).dst << "---" << (*it).weight << endl;
	}
}
Test:KruskalMST
#include "Graph.h"
int main(int argc, char ** argv) {
	Graph g(9);
	g.addEdge(Edge(0, 1, 4));
	g.addEdge(Edge(0, 7, 8));
	g.addEdge(Edge(1, 2, 8));
	g.addEdge(Edge(2, 8, 2));
	g.addEdge(Edge(1, 7, 11));
	g.addEdge(Edge(7, 8, 7));
	g.addEdge(Edge(7, 6, 1));
	g.addEdge(Edge(6, 8, 6));
	g.addEdge(Edge(6, 5, 2));
	g.addEdge(Edge(2, 5, 4));
	g.addEdge(Edge(2, 3, 7));
	g.addEdge(Edge(3, 5, 14));
	g.addEdge(Edge(3, 4, 9));
	g.addEdge(Edge(4, 5, 10));
	g.KruskalMST();
	system("pause");
}

3.3 Prim算法實現MST

示例:原圖來自

原理:1. 若圖中有結點自己成環,刪掉;若圖中兩相鄰結點有多條邊,只保留最小邊。 2. 分成兩個區域,已選區域和未選區域。開始:選取一點到已選區 。3. 已選區會有邊去往未選區,優先選擇最小的邊,並移動相應的頂點到已選區域。再重複同樣的步驟。

用鄰接矩陣來表示圖,對於未直接相連的點,用INF表示。數組selected[] = {},數組unselected[] ={},每次從unselected中選取路徑最短的,非鄰居節點的距離爲INF,故不會被選取到。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-EpIXviLe-1583824845611)(圖\4_1.png)]

額,這裏代碼的具體實現,主要是如何檢測最小邊,我寫的代碼中通過遍歷selected數組來選取最小邊,複雜度較高。如圖所示的這種方法,通過不斷覆蓋來判斷一個點連接到mst的最小權重,更優化。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9rzeRY11-1583824845616)(圖\4_2.png)]

Code : Prim
#pragma once
#include<iostream>
#include<vector>
using namespace std;

class Graph {
public:
	int V, E;
	vector<vector<int>> adj;
	Graph(int v):V(v) {
		for (int i = 0; i < v; i++)
		{
			vector<int> temp;
			for (int j = 0; j < v; j++)
			{
				temp.push_back(INT16_MAX);
			}
			adj.push_back(temp);
		}
	}
	void addEdge(int src, int dst, int weight);
	void PrimMST();
	vector<int> findmin(vector<bool> selected);
	void printMST(vector<int> mst, vector<int> weight);

};


void Graph::addEdge(int src, int dst, int weight) {
	adj[src][dst] = weight;
	adj[dst][src] = weight;
	E++;
}

void Graph::PrimMST() {
	vector<bool> selected(V,false);
	selected[0] = true;
	int i = 1; // 用於記錄被選中的個數
	int key = 0; // 用於記錄最新被選中的節點id
	vector<int> mst(V, -1);   // 存放父節點
	vector<int> weight(V, 0); // 存放MST權重
	while (i < V) {
		vector<int> a = findmin(selected);
		//a[0]:parent,a[1]:minid,a[2]:minvalue
		selected[a[1]] = true;
		mst[a[1]] = a[0];
		weight[a[1]] = a[2];
		key = a[1];
		i++;
	}
	printMST(mst, weight);
}
vector<int> Graph::findmin(vector<bool> selected) {
	int min = INT16_MAX;
	int min_id;
	int parent;
	for (int j = 0; j < V; j++){
		if (selected[j])
		{
			for (int i = 0; i < V; i++) {
				if (selected[i] == false && adj[j][i] < min) {
					min = adj[j][i];
					min_id = i;
					parent = j;
				}
			}
		}
	}
	vector<int> a{ parent,min_id,min };
	return a;
}


void Graph::printMST(vector<int> mst,vector<int> weight) {
	for (int i = 0; i < V; i++){
		cout << "weight:"<< weight[i]<<"  node:"<<i<<"---"<< mst[i] << endl;
	}
}
Test : Prim
#include "Graph.h"
#include<iostream>
int main(int argc, char ** argv) {

	Graph g(9);
	g.addEdge(0, 1, 4);
	g.addEdge(0, 7, 8);
	g.addEdge(1, 7, 11);
	g.addEdge(1, 2, 8);
	g.addEdge(2, 8, 2);
	g.addEdge(7, 8, 7);
	g.addEdge(8, 6, 6);
	g.addEdge(7, 6, 1);
	g.addEdge(6, 5, 2);
	g.addEdge(2, 5, 4);
	g.addEdge(2, 3, 7);
	g.addEdge(3, 5, 14);
	g.addEdge(3, 4, 9);
	g.addEdge(4, 5, 10);
	g.PrimMST();
	system("pause");
}

四、拓撲排序Topological Sorting

拓撲排序:將有向無環圖的頂點排成一個線性序列,如果一個圖有環,則無法找到拓撲排序,因爲環內的度不可能爲0。另外,拓撲排序不唯一。

應用背景:

  1. 任務流程圖。例如,學習人工智能前需要先學習數學和編程…先穿褲子,再穿鞋
  2. 課程安排,預編譯庫,樹的層序遍歷就是一種拓撲排序

4.1 兩種理解

  1. 找到所有頂點的入度,確定入度爲0的頂點爲起點,刪掉該頂點及該頂點所有的出邊

  2. 重新統計所有頂點的入度,確定入度爲0的頂點爲起點,刪掉該頂點和所有出邊

  3. 重複上述步驟,若有兩個頂點度爲0,隨便選一個。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YVB1ga2K-1583824845623)(圖\5_1.png)]

  1. 任意選擇一個頂點開始DFS遍歷,當一個結點的鄰居結點都被訪問後,將該結點壓入棧中

  2. 對未訪問的節點進行相同操作,注意結果逆向排列。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HLn7sPu7-1583824845627)(圖\5_2.png)]

4.2 具體實現步驟

  • 設置一個visited數組或者set,和一個stack。選取一個結點開始訪問,放入visited中。對其子節點進行相同操作,當某一子節點不再有鄰居結點時,將其放入stack。然後回到父節點,訪問其他子節點。當一顆DFS樹被遍歷之後,遍歷其他DFS森林,放入stack中,最後拓撲排序結果從stack中一一彈出。
  • 其實,修改一下DFS代碼即可

4.3 代碼實現

Code:Topological Sorting
#pragma once
#include <iostream>
#include <list>
#include <queue>
#include <stack>
#include <vector>
using namespace std;
class AdjlistGraph
{
	int V;
	vector<list<int>> adj;
public:
	AdjlistGraph(int _V) :V(_V) {
		for (int i = 0; i < _V; i++)
		{
			list<int> ls;
			ls.push_back(i);
			adj.push_back(ls);
		}
	}
	void addEdge(int a, int b);
	void topoSort();
	void topoSort(int start, vector<bool> &visited,stack<int> &topos);
};

void AdjlistGraph::addEdge(int a, int b) {
	adj[a].push_back(b);
}

void AdjlistGraph::topoSort() {
	vector<bool> visited(V, false);
	stack<int> topos;
	for (int i = 0; i < V; i++){
		if (visited[i] != true) {
			topoSort(i, visited,topos); 
		}
	}
	while (!topos.empty()>0)
	{
		cout << topos.top() << " ";
		topos.pop();
	}
}
// 一顆DFS樹進行拓撲排序
void AdjlistGraph::topoSort(int start, vector<bool> &visited,stack<int> &topos) {
	stack<int> s;
	s.push(start);
	visited[start] = true;
	while (!s.empty())
	{
		int v = s.top();
		auto i = ++adj[v].begin();
		bool flag = false;
		for (; i != adj[v].end(); i++) {
			// 去找鏈表中的未訪問結點,找到就退出
			if (!visited[(*i)])
			{
				flag = true;
				break;
			}
		}
		if (flag == true)
		{
			s.push(*i);
			visited[(*i)] = true;
		}
		else {
			topos.push(s.top());
			s.pop();
		}
	}
}
Test:Topological Sorting
#include "AdjlistGraph.h"
int main(int argc, char ** argv) {
	AdjlistGraph g(8);
	g.addEdge(0, 2);
	g.addEdge(2, 4);
	g.addEdge(4, 6);
	g.addEdge(6, 7);
	g.addEdge(1, 3);
	g.addEdge(3, 4);
	g.addEdge(3, 5);
	
	cout << endl << "************" << endl;
	g.topoSort();
	cout << endl << "************" << endl;

	system("pause");
	return 0;
}

五、強連通分量 SCC

連通圖:圖中任意兩個頂點之間有路可到達。

連通分量:無向圖的某個極大子圖符合連通圖的性質,則稱子圖爲連通分量。(無向圖的連通分量很好寫,修改DFS森林的代碼即可,單個DFS本身就是一個連通分量。)

弱連通圖:將有向圖的所有邊換成無向邊後,若是連通的,則有向邊爲弱連通圖

強連通圖:有向圖中任意兩個頂點都存在相互到達的路徑

強連通分量(Strongly Connected Components):一張有向圖G的極大強連通子圖G‘。強連通分量和強連通分量之間不會形成環。若將每個強連通分量縮成一個點,則原圖G得到的Componet Graph變成一張有向無環圖DAG。(有向環是強連通分量)。

應用:很多個文件,化爲多個強連通子圖(多個模塊),將原來的文件化爲有向無環圖,得到模塊之間的依賴關係。如果修改了某一個模塊,可以根據依賴關係判斷其他模塊是否需要修改,可以節約測試成本。

有兩種常見的方法用於找到強連通分量:Kosaraju和Tarjan。

5.1 Kosaraju算法找SCC

5.1.1 操作

kosaraju算法進行兩次DFS,第一次在原圖上進行,並在結點所有鄰居節點都被訪問後,將結點壓入一個棧中,第二次DFS在G的反向圖GT(將鄰接矩陣轉置)上進行,並且初始點選擇棧中最上面的點,每次dfs所訪問的點構成一個強連通分量。

5.1.2 理解

Kosaraju的核心在於通過反轉和節點退出DFS的時間,封死連通分量往外走的路。

考慮反轉:Graph G的縮減圖DAG,對DAG進行遍歷會得到DFS樹假設爲C1->C2。而我們希望每次搜索都控制在SCC區域內,當C1結束後不再進入C2。如何做到呢?只要反向G得到GT。GT和G的SCC完全相同。對GT進行DAG,得到DFS樹即C1<-C2,C1無法到達C2,這樣就封死了C1往C2走的路。

下一步就是如何確保C1優先於C2被訪問以防止C2再走到C1呢?

考慮退出DFS的時間(finishtime):若G的DFS從C1->C2,則C1中至少有一個頂點A,會在C2中所有頂點都DFS結束之後才退出DFS(這個點往往是SCC之間的連接點,比如圖中的1,6,7。1會在C2進行DFS之後才退出DFS),因此C2先結束,將其被壓入棧中,A會在C2之後被壓入棧中。當反向G後,從A先棧中彈出,先於C2進行DFS,而A所在的這個C1,由於C1<-C2,已經無法通過DFS到達C2,只能在C1內部進行DFS,因而不會產生SCC交叉。

簡單地說,第一步對G進行DFS,找到原父點和原子SCC的順序(根據DFS的退出時間)。第二步反向G,得到GT。原父點所在的SCC變成新子SCC,根據步驟一所得的順序,先根據原父點對新子SCC進行DFS,再對原子SCC進行DFS,這樣就不會串門啦~ 簡單示例 : G:DFS,得到stack= [C2 1 6 C4 7|,GT:DFS(7),DFS(C4),DFS(1),DFS(C2)

複雜度分析:兩次DFS,時間複雜度O(E+V),一個stack,空間複雜度O(V)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MyRchA1K-1583824845633)(圖\6_2.png)]

5.1.3 具體例子

DFS G:只要頂點無路可走,就被壓入stack中

  • 初始化:visited=[],stack=[],stack用於從小到大記錄finish time

  • start=0,DFS:0->2->1->5->3->4,沒路了,4壓入棧,{5,3,1,2,0}同理。

    visited=[0,2,1,5,3,4],stack=[4,3,5,1,2,0]

  • start=8,DFS:8–>9->7->6,沒路了

    visited=[0,2,1,5,3,4,8,9,7,6],stack=[4,3,5,1,2,0,6]

  • DFS:7->10,沒路了

    visited=[0,2,1,5,3,4,8,9,7,6,10],stack=[4,3,5,1,2,0,6,10,7,9,8]

DFS GT:按照stack的彈出順序DFS

  • 初始化:visited=[],stack=[4,3,5,1,2,0,6,10,7,9,8]

  • start=8,DFS:8->6->7->9,得到一個SCC

    visited=[8,6,7,9],79visited,故取start=10

  • start=10,DFS:10,得到一個SCC,6visited,故取start=0

    visited= [8,6,7,9,10]

  • start=0,DFS:0->1->2,得到一個SCC,12visited,故取start=5

    visited= [8,6,7,9,10,0,1,2]

  • start=5,DFS:5->4->3,得到一個SCC,43visited,stack空,結束

    visited= [8,6,7,9,10,0,1,2,5,4,3] 共記4個SCC

5.1.4 代碼實現

Code:Kosaraju
#pragma once
#include<list>
#include<stack>
#include <vector>
#include<iostream>
using namespace std;
class Graph {
	int V;
	vector<list<int>> adj;
public:
	Graph(int _v) :V(_v) {
		for (int i = 0; i < _v; i++)
		{
			list<int> ls;
			adj.push_back(ls);
		}
	}
	void addEdge(int a, int b);
	void DFS(int start, vector<bool>& visited);
	void DFS(int start, vector<bool>& visited, stack<int> &order);
	Graph getTranspose();
	void getSCC();
};

void Graph::addEdge(int a, int b) {
	adj[a].push_back(b);
}
// 用於第二次DFS輸出SCC
void Graph::DFS(int start, vector<bool>& visited) {
	stack<int> s;
	s.push(start);
	std::cout << start << " ";
	visited[start] = true;

	while (!s.empty())
	{
		int v = s.top();
		auto i = adj[v].begin();
		bool flag = false;
		for (; i != adj[v].end(); i++)
		{
			if (!visited[(*i)]) {
				flag = true;
				break;
			}
		}
		if (flag == true)
		{
			s.push(*i);
			std::cout << *i << " ";
			visited[(*i)] = true;
		}
		else {
			s.pop();
		}
	}
}
// 用於第一次DFS記錄節點退出順序
void Graph::DFS(int start, vector<bool>& visited,stack<int> &order) {
	stack<int> s;
	s.push(start);
	visited[start] = true;

	while (!s.empty()){
		int v = s.top();
		list<int>::iterator i = adj[v].begin();
		bool flag = false;
		for (; i != this->adj[v].end(); i++)
		{
			if (!visited[*i]) {
				flag = true;
				break;
			}
		}
		if (flag == true)
		{
			s.push(*i);
			visited[*i] = true;
		}
		else {
			order.push(s.top());
			s.pop();
		} 
	}
}

Graph Graph::getTranspose() {
	Graph g(V);
	for (int i = 0; i < V; i++){
		list<int>::iterator it = this->adj[i].begin();
		for (; it!=adj[i].end(); it++){
			g.adj[*it].push_back(i);
		}
	}
	return g;
}

void Graph::getSCC() {
	// 第一輪DFS
	vector<bool> visited(V, false);
	stack<int> firstorder; // 根據退出DFS的時間存儲節點。
	for (int i = 0; i < V; i++){
		if (visited[i] != true){
			DFS(i, visited, firstorder);
		}
	}
	// 第二輪DFS
	Graph g = this->getTranspose();
	vector<bool> visitedtwice(V, false);
	while (!firstorder.empty()){
		int val = firstorder.top();
		if (visitedtwice[val]==false)
		{
			g.DFS(val, visitedtwice);
			cout << endl;
		}
		firstorder.pop();
	}
}
Test:Kosaraju
#include<iostream>
#include "Graph.h"
using namespace std;
int main(int argc, char** argv) {
	Graph gra(11);
	gra.addEdge(0, 2);
	gra.addEdge(2, 1);
	gra.addEdge(1, 0);
	gra.addEdge(1, 5);
	gra.addEdge(5, 3);
	gra.addEdge(4, 5);
	gra.addEdge(3, 4);
	gra.addEdge(6, 4);
	gra.addEdge(6, 8);
	gra.addEdge(8, 9);
	gra.addEdge(9, 7);
	gra.addEdge(7, 6);
	gra.addEdge(7, 10);

	gra.getSCC();

	system("pause");
	return 0;
}

5.2 Tarjan算法找SCC

5.2.1 簡單介紹

Tarjan的核心在於SCC中最先被訪問的頂點First,按照訪問順序它能追溯到的祖先就是自身。而SCC中的其他點則可追溯祖先到First。於是它用dfs[]記錄頂點的訪問順序,用low[]來記錄它能追溯的最早祖先。難點在於這個追溯的過程,假設a->b->c->d->a,當d追溯到了a,則b,c的追溯情況也會發生變化,這可以用遞歸解決,當一個頂點的某個鄰居節點都被DFS之後,立馬更新該頂點,也就是DFS(a)->DFS(b)->DFS( c)->DFS(d)->update(low[d])->update(low[c])->update[low(b)]->find(low(a)==dfs(a))->判斷a是FIRST。最後討論關於SCC的輸出,只要用棧來記錄訪問到的點,當判斷出First後,將low相同的點從棧中統統彈出即可。

可以關注一下:Tarjan的DFS採用先DFS鄰居頂點,再更新自身頂點屬性的方法(常見的遞歸寫法,需掌握)

5.2.2 算法流程

數據結構:

  • 變量time:記錄訪問到第幾個頂點了。

  • dfn[i]:記錄頂點是第幾個被DFS到的,每個頂點的時間戳,初始都設爲-1

  • low[i]:頂點在它的SCC中,能找到的最小時間戳,也就是( i 能找到的最早回邊)

  • stack[]:記錄當前已經訪問過,且未被彈出的節點。

假設:

  1. 開始:選擇一個點開始DFS,每到unvisited的點v就預設dfn[v]=low[v],壓入stack

  2. 判斷v的鄰居k是否visited(dfs[k]==-1)

    • 若 k not visited,就DFS(k),並在結束DFS後,判斷是否滿足low[k]<low[v],是則更新low[v],讓low[v]=low[k]

    • 若 k visited 且 k in stack,是則必有k爲v的祖先,low[k]<low[v],令low[v]=min(low[v],low[k])。

      (補充1:對於k visited but not in stack的情況,說明這個點已經被早早彈出去了,屬於其他SCC

      補充2:這裏還有另一種寫法是令low[v]=min(low[v],dfn[k])。在這裏都可以使用。但是對於求割點的情況,只能用low[v]=min(low[v],dfn[k]),否則會跳過割點回溯到更遠處。用這種寫法更加符合它的定義。)

  3. 當v的所有鄰居都結束訪問後,也就是low[v]被更新到最小的情況,判斷dfs[i]==low[i],若是則說明該點爲SCC的根節點(也就是是指SCC中被最先訪問的點,因爲low[v]不可能更小了),將stack中所有low爲low[i]的元素彈出,成爲一個SCC

5.2.3 例子

圖中(x,y)表示(dfn[i],low[i])

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dFz3V8Mh-1583824845640)(圖\6_3.png)]

5.2.4 代碼實現

Code:Tarjan for SCC
#pragma once
#include<vector>
#include<list>
#include<algorithm>
using namespace std;

class Graph {
public:
	int V;
	vector<list<int>> adj;
	Graph(int v) :V(v) {
		for (int i = 0; i < V; i++){
			list<int> ls;
			adj.push_back(ls);
		}
	}
	void addEdge(int v, int w);

	void getSCC();
	void SCC(int start, vector<int>& dfn, vector<int>& low, vector<int>& s, int& time);
};
void Graph::addEdge(int v, int w) {
	adj[v].push_back(w);
}
void Graph::getSCC() {
	vector<int> dfn(V, -1);
	vector<int> low(V, -1);
	vector<int> s;
	int time = 0;
	int i = 0;
	for (int i = 0; i < V; i++)
	{
		if(dfn[i] == -1) {
			SCC(i, dfn, low, s, time);
		}
	}
}
void Graph::SCC(int start, vector<int>& dfn, vector<int>& low, vector<int>& s, int& time){
	dfn[start] = time++;
	low[start] = dfn[start];
	s.push_back(start);
	
 	for (auto it = adj[start].begin(); it != adj[start].end(); it++){
		if (dfn[*it] == -1) { //unvisited
			SCC(*it, dfn, low, s, time);
			low[start] = min(low[start], low[*it]);
		}
		else{
			auto sit = find(s.begin(), s.end(), *it);
			if (sit != s.end()) // 元素visited並且in stack
			{
				low[start] = min(low[start], low[*it]);
			}
		}
	}
	if (dfn[start] == low[start]){
		while (!s.empty()&&(low[s.back()] == low[start])) {
			cout << s.back() << " ";
			s.pop_back();
		}
		cout << endl;
	}
}
Test:Tarjan for SCC
#include<iostream>
#include "Graph.h"

using namespace std;

int main(int argc, char** argv) {
	Graph gra(11);
	gra.addEdge(0, 2);
	gra.addEdge(2, 1);
	gra.addEdge(1, 0);
	gra.addEdge(1, 5);
	gra.addEdge(5, 3);
	gra.addEdge(4, 5);
	gra.addEdge(3, 4);
	gra.addEdge(6, 4);
	gra.addEdge(6, 8);
	gra.addEdge(8, 9);
	gra.addEdge(9, 7);
	gra.addEdge(7, 6);
	gra.addEdge(7, 10);

	gra.getSCC();
	system("pause");
	return 0;
}

5.2.5 和Kosaraju比較

Kosaraju兩次DFS,時間複雜度也是O(V+E)。Tarjan只用對原圖進行一次DFS,時間複雜度也是O(V+E),但常數項更小。在實際的測試中,Tarjan算法的運行效率比Kosaraju算法高30%左右。

5.2.5 割點、橋、雙連通分量

割點Articulation Point(AP):無向連通圖,去掉一個點,圖就不再連通,則該點是割點。比如圖中頂點1

橋Bridge:無向連通圖中,去掉某條邊,圖不連通,則該邊爲橋。比如圖中連接16的邊。
img src="圖\0_2.png" alt="2" style="zoom:50%;" />

5.2.5.1 割點AP的求法:

粗糙的方法:是一個接一個地刪除所有頂點,並查看頂點的刪除是否會導致圖形斷開,用DFS或BFS,複雜度太高O(V*(V+E))。

牛掰的方法:用Tarjan啊~

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-2I9V75S3-1583824845642)(圖\6_4.png)]

因此,從圖中可以看出,v->k,割點v滿足以下條件:

  • v是DFS樹的root,並且在DFS樹中至少有兩個孩子
  • v不是DFS樹的root,且low[k]>=dfn[v]

DFS樹的葉子不可能是割點

5.2.5.2 橋Bridge 的求法

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-thyPI004-1583824845648)(C:\Users\daiia\Desktop\圖\6_5.png)]

因此,從圖中可以看出,v->k,橋vk滿足以下條件:low[k]>dfn[v]

5.2.5.3 例子

在這裏插入圖片描述

5.2.5.4 雙連通分量BCC的概念和求法
  • 點雙連通:刪掉一個點之後,圖仍聯通—>無割點—>消除割點的辦法:任意兩邊必在一個環中->->任意兩點至少存在兩條無公共頂點的路徑(除起點和終點)
  • 邊雙連通:刪掉一條邊之後,圖仍聯通—>無橋—>消除橋的辦法:每條邊都在至少一個環內->任意兩點至少存在2條無公共便的路徑

點雙連通分量v-BCC:無向圖的極大點雙連通子圖。

求法:求解點雙連通分量,可以先求割點,但是割點屬於多個v-BCC,其餘頂點只屬於一個v-BCC。這在輸出時有困難。解決辦法:在DFS時,將DFS樹的邊壓入棧中,找到割點後,開始取棧中和割點有關的邊

開一個棧,tarjan遞歸訪問到某個點的時候入棧,然後每次經過一條邊(x,y)(x,y)而且x滿足low[y]>=dfn[x]low[y]>=dfn[x]的時候不管x是不是割點,都把棧裏的元素一一彈出,直到把y彈出,所有彈出的點,再加上x,構成一個點雙。

邊雙連通分量e-BCC:無向連通圖的極大邊雙連通子圖。

求法:用Tarjan求出無向圖中所有的橋,將無向圖分成多塊,每塊都是e-BCC,在訪問時不走橋即可。將邊雙連通分量縮點後,得到的邊都是橋。

5.2.5.5 代碼實現
Code:AP-Tarjan
#pragma once
#include<vector>
#include<list>
#include<algorithm>
using namespace std;

class Graph {
public:
	int V;
	vector<list<int>> adj;
	Graph(int v) :V(v) {
		for (int i = 0; i < V; i++) {
			list<int> ls;
			adj.push_back(ls);
		}
	}
	void addEdge(int v, int w);

	void getAP();
	void AP(int v, vector<int>& dfn, vector<int>& low, int& time, vector<int>& parent);
};
void Graph::addEdge(int v, int w) {
	adj[v].push_back(w);
	adj[w].push_back(v);
}
void Graph::getAP() {
	vector<int> dfn(V, -1);
	vector<int> low(V, -1);
	vector<int> parent(V, -1);
	int time = 0;
	int i = 0;
	for (int i = 0; i < V; i++)
	{
		if (dfn[i] == -1) {
			AP(i, dfn, low, time,parent);
		}
	}
}
void Graph::AP(int v, vector<int>& dfn, vector<int>& low, int& time, vector<int>& parent) {
	dfn[v] = time++;
	low[v] = dfn[v];

	int child = 0;//記錄當前節點的DFS孩子數

	for (auto it = adj[v].begin(); it != adj[v].end(); it++) {
		if (dfn[*it] == -1) { //unvisited
			parent[*it] = v;
			child++;
			AP(*it, dfn, low,time,parent);
			low[v] = min(low[v], low[*it]);
		}
		else {
			if(*it!=parent[v])	 low[v] = min(low[v], dfn[*it]);
			//這裏注意啊,不能讓他回溯到祖先的祖先,這樣會跳過割點
			//也不能回溯到父節點,這樣還搞啥子哦
		}
	}
	if (parent[v] == -1) { // v是root的情況
		if (child > 1) cout << v << " ";
	}else if (parent[parent[v]] != -1 && low[v] >= dfn[parent[v]]) {
		// parent[v]非root節點是割點的情況
		cout << parent[v] << " ";
	}
}
Code:Bridge-Tarjan
#pragma once
#include<vector>
#include<list>
#include<algorithm>
using namespace std;

class Graph {
public:
	int V;
	vector<list<int>> adj;
	Graph(int v) :V(v) {
		for (int i = 0; i < V; i++) {
			list<int> ls;
			adj.push_back(ls);
		}
	}
	void addEdge(int v, int w);

	void getBridge();
	void Bridge(int v, vector<int>& dfn, vector<int>& low, int& time, vector<int>& parent);
};
void Graph::addEdge(int v, int w) {
	adj[v].push_back(w);
	adj[w].push_back(v);
}
void Graph::getBridge() {
	vector<int> dfn(V, -1);
	vector<int> low(V, -1);
	vector<int> parent(V, -1);
	int time = 0;
	int i = 0;
	for (int i = 0; i < V; i++)
	{
		if (dfn[i] == -1) {
			Bridge(i, dfn, low, time,parent);
		}
	}
}
void Graph::Bridge(int v, vector<int>& dfn, vector<int>& low, int& time, vector<int>& parent) {
	dfn[v] = time++;
	low[v] = dfn[v];

	for (auto it = adj[v].begin(); it != adj[v].end(); it++) {
		if (dfn[*it] == -1) { //unvisited
			parent[*it] = v;
			Bridge(*it, dfn, low,time,parent);
			low[v] = min(low[v], low[*it]);
		}
		else {
			if(*it!=parent[v])	 low[v] = min(low[v], dfn[*it]);
			//這裏注意啊,不能讓他回溯到祖先的祖先,這樣會跳過割點
			//也不能回溯到父節點,這樣還搞啥子哦
		}
	}
	if (parent[v]!=-1 && low[v] > dfn[parent[v]])
		cout << v << "-" << parent[v] << endl;
}
Test:Tarjan for AP/Bridge
#include<iostream>
#include "Graph.h"
using namespace std;

int main(int argc, char** argv) {
	Graph gra(8);
	gra.addEdge(0, 2);  gra.addEdge(2, 1);	gra.addEdge(3, 0);
	gra.addEdge(2, 4);	gra.addEdge(4, 3);	gra.addEdge(3, 7);	
    gra.addEdge(7, 5);	gra.addEdge(6, 7);	gra.addEdge(6, 5);
	gra.getAP();
    gra.getBridge();
	system("pause");
	return 0;
}

六、最短路徑

最短路徑問題shortest path problem:找到兩點之間邊權和最短的路。

  • 單源最短路徑問題:固定一個頂點爲源點,求源點到其他每個點的最短路徑

    • 邊權非負:Dijkstra — O(V^2),可優化O(VlogV+E)
    • 邊權可爲負,但不能有負環:Bellman-Ford — O(VE)
    • Bellman-Ford改進:SPFA:複雜度不穩定,但有些情況真的好用。
  • 多源最短路徑問題:計算每個點對之間的最短路

    • Floyd-Warshall —O(V^3)

6.1 Dijkstra

還記得Prim算法麼,就分倆區找最短邊連起來的那個。Dijkstra類似Prim算法,用貪心做的。注意Dijkstra算法不可以處理負邊!

6.1.1 算法流程

以給定源爲根。分兩個區,A區存放**SPT(Shortest path tree)**上的點,B區存放非SPT樹上的點。每次執行,從B區找到一個到源距離最短的點。

流程:

  • 創建空 set A以追蹤SPT上的點。
  • 創建距離數組,並初始化所有距離爲無窮 distance(V,INFINITE),修改源的距離爲0。
  • 當set A未滿員
    • 選擇一個最小距離且不處於set A的點u
    • 將u吸收到A區
    • 更新u的所有鄰居節點的距離值。具體通過迭代所有頂點,對於u的每個鄰居頂點v,if distance[u]+edgeweight(u->v) < distance[v],then update distance[v].

複雜度分析:

​ 要找到去往V-1個頂點的路徑,在每次查找過程中,會遍歷其鄰居節點,最壞的情況,鄰居節點有V-1個,因此最壞時間複雜度O(V^2)。如果優化查找最小距離點的算法,比如用斐波那契堆實現,則複雜度降低爲O(VlogV+E)。斐波那契堆回頭再補充啊~

缺點分析:

​ Dijkstra算法不可以處理負邊。因爲默認set A中的點,都已經找到了從源點到這個點的最短路徑。若存在負邊,則默認不成立,加上負邊後,可以比最短路徑更短。

6.1.2 例子

原圖地址

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QRNfjbbD-1583824845660)(圖\7_1.png)]

6.1.3 代碼

Code:Dijkstra(未用堆優化)
#pragma once
#include<set>
#include<iostream>
#include<vector>
#include<list>
#include<algorithm>
using namespace std;
#define INF INT_MAX

class Node {
public:
	int dst;
	int weight;
	Node(int d, int w):dst(d),weight(w) {}
};

class Graph {
public:
	int V;
	vector<list<Node>> adj;
	Graph(int v) :V(v) {
		for (int i = 0; i < v; i++)
		{
			list<Node> ls;
			adj.push_back(ls);
		}
	}
	void addEdge(int a, int b,int weight);
	vector<int> Dijkstra(int start);
};

void Graph::addEdge(int a, int b,int weight) {
	adj[a].push_back(Node(b, weight));
	adj[b].push_back(Node(a, weight));
}

vector<int> Graph::Dijkstra(int start) {
	set<int> A;
	vector<int> distance(V, INF);
	distance[start] = 0;
	while (A.size()<V)
	{
		//int minu = findmin(vector<int> distance);
		int minu;
		int minval = INF;
		for (int i = 0; i < V;i++) {
			if (A.find(i)==A.end() && distance[i] <= minval) {
				minu = i;
				minval = distance[i];
			}
		}

		A.insert(minu);

		//updatedistance(int minvertex);
		list<Node>::iterator it = adj[minu].begin();
		for (; it != adj[minu].end(); it++) {
			distance[(*it).dst] = min(distance[minu] + (*it).weight, distance[(*it).dst]);
		}
	}
	return distance;
}
Test:Dijkstra
#include<iostream>
#include "Graph.h"
using namespace std;
int main(int argc, char** argv) {
	Graph g(9);
	g.addEdge(0, 1, 4);	g.addEdge(0, 7, 8);	g.addEdge(1, 7, 11);
	g.addEdge(1, 2, 8);	g.addEdge(2, 8, 2);	g.addEdge(7, 8, 7);
	g.addEdge(8, 6, 6);	g.addEdge(7, 6, 1);	g.addEdge(6, 5, 2);
	g.addEdge(2, 5, 4);	g.addEdge(2, 3, 7); g.addEdge(3, 5, 14);
	g.addEdge(3, 4, 9);	g.addEdge(4, 5, 10);

	vector<int> a = g.Dijkstra(0);
	for (int k=0;k<9;k++){
		cout << k << " distance:" << a[k]<<endl;
	}

	system("pause");
	return 0;
}

6.2 Bellman-Ford

Dijkstra無法處理負邊的情況,BF算法可以,但算法複雜度高於Dijkstra。

循環 i from 1 to V-1次(每次循環都是去找從源點出發最多經過 i 條邊能找到的最短路徑長度),每次循環下都遍歷所有的邊,判斷是否有權值和更小的情況,有就更新,直到距離數組不發生變化爲止。不適用於有負環的情況。有點暴力?待學了DP再看一遍。

6.2.1 算法流程

流程:

創建距離數組,並初始化所有距離爲無窮 distance(V,INFINITE),修改源的距離爲0。

將所有邊放到edgelist。

循環以下操作V-1次:// 每次循環都是去找從源點出發最多經過i 條邊能找到的最短路徑長度

  • 對於edgelist中的每條邊,判斷 if distance[u]+edgeweight(u-v) < distance[v],then update distance[v]=distance[u]+edgeweight(u-v) .
  • 若這次循環沒有更新distance[],則結束

那麼問題來了,如果循環V-1次後,退出循環,可以確定是最短路徑嗎?

答:若存在負環(環的權值和爲負數),最短距離會陷入死局。具體看圖例。如果環的權值和是非負數,則可行。

拓展:Bellman-Ford算法可以檢測權值和爲負數的環,只要在循環結束之後再進行一次判斷,如果還存在distance[u]+edgeweight(u-v) < distance[v],就能找到負環

複雜度分析:

對V-1次循環,每次循環對edgelist遍歷,複雜度爲O(E*(V-1))=O(VE),最慘的情況:邊數最大,E=V(V-1)/2,複雜度O(V^3)。

缺點分析:無法解決存在負環的情況

6.2.2 例子

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8KGcvVmr-1583824845663)(圖\7_2.png)]

在這裏插入圖片描述

6.2.3 代碼

Code:Bellman-Ford
#pragma once
#include<iostream>
#include<vector>
using namespace std;

class Edge {
public:
	int src;
	int dst;
	int weight;
	Edge(int s, int d, int w) :src(s), dst(d), weight(w) {}
};

class Graph {
public:
	int V;
	vector<Edge> edgelist;

	Graph(int v):V(v) {}
	void addEdge(int a, int b, int weight);
	vector<int> BellmanFord(int start);
};
void Graph::addEdge(int a, int b,int weight) {
	edgelist.push_back(Edge(a,b,weight));
}

vector<int> Graph::BellmanFord(int start) {
	vector<int> distance(V, INT_MAX);
	distance[start] = 0;
	for (int i = 1; i < V; i++){
		vector<Edge>::iterator eit;
		bool flag = false;
		for (eit=edgelist.begin();eit!=edgelist.end();eit++){
			if (distance[(*eit).src] + (*eit).weight < distance[(*eit).dst]) {
				distance[(*eit).dst] = distance[(*eit).src] + (*eit).weight;
				flag = true; // distance被更新
			}
		}
		if (!flag) 	break;
	}
	return distance;
}
Test:Bellman-Ford
#include<iostream>
#include "Graph.h"

using namespace std;
int main(int argc, char** argv) {
	Graph g(7);
	g.addEdge(0, 1, 6);		g.addEdge(0, 2, 5);		g.addEdge(0, 3, 5);
	g.addEdge(2, 1, -2);	g.addEdge(3, 2, -2);	g.addEdge(1, 4, -1);
	g.addEdge(2, 4, 1);		g.addEdge(4, 6, 3);		g.addEdge(3, 5, -1);
	g.addEdge(5, 6, 3);
	vector<int> distance = g.BellmanFord(0);
	for (auto i = distance.begin(); i < distance.end(); i++){
		cout << *i << " ";
	}
	system("pause");
	return 0;
}

6.3 隊列優化的Bellman-Ford

這個算法吧,Shortest Path Faster Algorithm:優化了Bellman-Ford,減少其中冗餘的判斷。加了個隊列來維護。但是它的時間複雜度也沒有優化很多。

6.3.1 算法流程

算法流程:

  • 初始:將源點加入隊列

  • 每次從隊列中取出一個頂點,並對鄰居點進行更新(方法和Bellman-Ford一樣),更新成功就加入隊列,不更新就不用加入。重複過程,直到隊列爲空。

    這樣就排除了沒有更新過的點。

負環判斷

只要一個頂點進入隊列次數>n次,則存在負權值迴路。

複雜度分析:

SPFA的論文中複雜度分析被打死了,沒有快很多,玄學複雜度。但有些情況真的好用。

Code:SPFA
#pragma once
#include<iostream>
#include<vector>
#include<list>
#include<queue>
using namespace std;

class Node {
public:
	int dst;
	int weight;
	Node(int d, int w):dst(d), weight(w) {}
};

class Graph {
public:
	int V;
	vector<list<Node>> adj;

	Graph(int v):V(v) {
		for (int i = 0; i < v; i++){
			list<Node> ls;
			adj.push_back(ls);
		}
	}
	void addEdge(int a, int b, int weight);
	vector<int> SPFA(int start);
};
void Graph::addEdge(int a, int b,int weight) {
	adj[a].push_back(Node(b,weight));
}

vector<int> Graph::SPFA(int start) {
	vector<int> distance(V, INT_MAX);
	queue<int> que;
	distance[start] = 0;
	que.push(start);
	while (!que.empty()) {
		int u = que.front();
		que.pop();
		for (auto eit=adj[u].begin();eit!=adj[u].end();eit++){
			if (distance[u] + (*eit).weight < distance[(*eit).dst]) {
				distance[(*eit).dst] = distance[u] + (*eit).weight;
				que.push((*eit).dst);
			}
		}
	}
	return distance;
}
Test:SPFA
#include<iostream>
#include "Graph.h"

using namespace std;
int main(int argc, char** argv) {
	Graph g(7);
	g.addEdge(0, 1, 6);		g.addEdge(0, 2, 5);		g.addEdge(0, 3, 5);
	g.addEdge(2, 1, -2);	g.addEdge(3, 2, -2);	g.addEdge(1, 4, -1);
	g.addEdge(2, 4, 1);		g.addEdge(4, 6, 3);		g.addEdge(3, 5, -1);
	g.addEdge(5, 6, 3);
	vector<int> distance = g.SPFA(0);
	for (auto i = distance.begin(); i < distance.end(); i++){
		cout << *i << " ";
	}
	system("pause");
	return 0;
}

6.4 Floyd-Warshall

先看例子,再看流程

6.4.1 算法流程

流程:

輸入:原圖矩陣A(-1)。

  • 創建矩陣A(0),填入A(-1)中0所在的行和列以及所有對角線元素。也就是令A(0)[0,i]=A(-1)[0,i],A(0)[i,0]=A(-1)[i.0],A(0)[i,i]=0

  • 比較以0爲中間點的最短路徑,也就是令A(0)[i,j]=min{A(-1)[i,j],A(-1)[i,0]+A(-1)[0,j]}。若在A(-1)中,存在A[i,j] > A[i,0]+A[0,j],則說明通過中間點的路徑比兩點直達的路徑更短。新創建的矩陣A(0)中需要填入二者中更小的一個。

  • 對於其他所有的頂點進行類似操作,共新建V個矩陣,每次都根據上一次得到的矩陣結果判斷,A(k)[i,j]=min{A(k-1)[i,j],A(k-1)[i,0]+A(-1)[0,j]}

複雜度分析:

共對V個矩陣進行了V*V次判斷,O(n^3)

6.4.2 例子

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-QKI0Riq3-1583824845670)(圖\7_4.png)]

6.4.3 代碼實現

Code: Floyd-Warshall
#pragma once
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
#define INF INT_MAX

class Graph {
public:
	int V;
	vector<vector<int>> adj;

	Graph(int v):V(v) {
		for (int i = 0; i < v; i++){
			vector<int> ls(V,INF);
			adj.push_back(ls);
		}
		for (int i = 0; i < v; i++)
		{
			adj[i][i] = 0;
		}
	}
	void addEdge(int a, int b, int weight);
	vector<vector<int>> FloydInside(int k, vector<vector<int>> matrx);
	vector<vector<int>> Floyd();
};
void Graph::addEdge(int a, int b,int weight) {
	adj[a][b] = weight;
}

vector<vector<int>> Graph::Floyd() {
	vector<vector<int>> res=adj;
	for (int i = 0; i < V; i++){
		res =FloydInside(i, res);
	}
	return res;
}
vector<vector<int>> Graph::FloydInside(int k, vector<vector<int>> matrx) {
	vector<vector<int>> newmatra=matrx;
	for (int i = 0; i < V; i++){
		for (int j = 0; j < V; j++){
			if (i == j || i == k || j == k) continue;
			else if (matrx[i][k] != INF && matrx[k][j] != INF && (matrx[i][j] > matrx[i][k] + matrx[k][j]))
					newmatra[i][j] = matrx[i][k] + matrx[k][j];
		}
	}
	return newmatra;
}
Test:Floyd-Warshall
#include<iostream>
#include "Graph.h"

using namespace std;
int main(int argc, char** argv) {
	Graph g(4);
	g.addEdge(0, 1, 3);	g.addEdge(0, 3, 7); g.addEdge(1, 0, 8);
	g.addEdge(1, 2, 2);	g.addEdge(2, 0, 5);	g.addEdge(2, 3, 1);
	g.addEdge(3, 0, 2);
	vector<vector<int>> a = g.Floyd();
	for (int i = 0; i < 4; i++){
		for (int j = 0; j < 4; j++){
			cout << a[i][j] << " ";
		}
		cout << endl;
	}
	system("pause");
	return 0;
}

6.5 Johnson

處理多源最短路徑問題有多種思路,除了Floyd-Warshall還有暴力解,對於無負邊的情況,在Dijkstra外面在套一層循環。對於有負邊的情況,在Bellman-Ford外面套一層循環,若不考慮Dijkstra的堆優化,這三種情況的時間複雜度都爲O(n^3),但是Dijkstra可用堆優化成O(VlogV+E),所以套一層可以優化成O(V*(VlogV+E)),又但是,Dijkstra無法處理存在負邊的情況,那把所有邊都加k,使所有邊都爲正可行嗎?No,要考慮到步數。最短路徑會加上k * 步數。這時,Johnson提出了Johnson算法,加上一個數,令邊都爲正,而不影響最短路徑。他的輔助線是一個可以到達所有頂點且距離爲0的虛擬點。

6.5.1 原理

邊權賦值原理:讓uv邊權加上uv到虛擬點S的距離差

假設新建虛擬點 S 到 u 的最短距離爲fuf_u,單源最短距離可用SPFA求。

uuvv 的邊權爲 w(u,v)w(u,v),對邊權重新賦值後,令邊權爲
w(u,v)=w(u,v)+fufv w'(u,v)=w(u,v)+f_u-f_v
假設uv之間的最短路徑經過u,x1,x2,...,xk,vu,x_1,x_2,...,x_k,v,則最短路徑爲
dist(u,v)=w(u,x1)+w(x1,x2)+...+w(xk,v) dist(u,v)=w(u,x_1)+w(x_1,x_2)+...+w(x_k,v)
重新賦值後,最短路徑爲
dist(u,v)=w(u,x1)+w(x1,x2)+...+w(xk,v) dist'(u,v)=w'(u,x_1)+w'(x_1,x_2)+...+w'(x_k,v)
代入新邊權
dist(u,v)=w(u,x1)+fufx1+w(x1,x2)+fx1fx2...+w(xk,v)+fxkfv dist'(u,v)=w(u,x_1)+f_u-f_{x_1} +w(x_1,x_2)+f_{x_1}-f_{x_2} ... +w(x_k,v)+f_{x_k}-f_v
化簡
dist(u,v)=dist(u,v)+fufv dist'(u,v)=dist(u,v)+f_u-f_v
結果說明:

因爲u->v,所以,fu+w(u,v)>=fvf_u+w(u,v)>=f_v,新的邊權非負,並且從化簡結果可知最短路徑只變化了一個固定的數,這個數可以用Bellman-Ford虛擬點求出。最後再對所有頂點Dijkstra即可。

6.5.2 流程

  1. 新建一個虛擬的頂點S,令該頂點到所有其他點的距離爲0
  2. 用優化的Bellman-Ford(SPFA),計算S到其他頂點的最短距離 fkf_k
  3. 移除虛擬頂點S,重新賦值邊權。w(u,v)=w(u,v)+fufvw'(u,v)=w(u,v)+f_u-f_v
  4. 對每個頂點Dijkstra+斐波拉契堆,求得最短路徑。
  5. 所有最短路徑都減掉fufvf_u-f_v

6.5.3 新邊權例子

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nyzCZMFs-1583824845673)(圖\7_5.png)]

拎出來單獨重點歸納提一下

DFS分爲兩種,假設DFS路徑爲a->b->c->d

  • 先訪問當前節點,再遞歸相鄰節點,則訪問順序爲a,b,c,d,輸出順序爲a,b,c,d
  • 先遞歸相鄰節點,再訪問當前節點,則訪問順序爲a,b,c,d,輸出順序爲d,c,b,a。這種在Tarjan等算法中很有用

參考資料

  1. https://www.geeksforgeeks.org/graph-and-its-representations/
  2. https://www.youtube.com/watch?v=0u78hx-66Xk&list=PLqM7alHXFySEaZgcg7uRYJFBnYMLti-nh&index=2
  3. https://www.bilibili.com/video/av47042691?from=search&seid=9406491766784608254
  4. https://www.hackerearth.com/zh/practice/algorithms/graphs/minimum-spanning-tree/tutorial/ MST
  5. https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-algorithm-greedy-algo-2/ MST
  6. https://www.geeksforgeeks.org/union-find/ 並查集
  7. https://www.youtube.com/watch?v=4ZlRH0eK-qQ
  8. https://www.youtube.com/watch?v=ZtZaR7EcI5Y&list=PLdo5W4Nhv31bK5n8-RIGhvYs8bJbgJFDR&index=5
  9. https://blog.csdn.net/u014665013/article/details/51351371
  10. https://www.youtube.com/watch?v=VJnUwsE4fWA 並查集
  11. https://www.youtube.com/watch?v=wU6udHRIkcc並查集
  12. https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-algorithm-greedy-algo-2/ Kruskal
  13. http://www.codebelief.com/article/2017/04/prim-algorithm-introduction/ Prim
  14. https://www.cnblogs.com/skywang12345/p/3711507.html Prim
  15. https://www.geeksforgeeks.org/prims-minimum-spanning-tree-mst-greedy-algo-5/ Prim
  16. https://www.cnblogs.com/bigsai/p/11489260.html Topological Sorting
  17. https://www.youtube.com/watch?v=eL-KzMXSXXI Topological Sorting
  18. https://en.wikipedia.org/wiki/Strongly_connected_component 強連通分量
  19. https://www.youtube.com/watch?v=RpgcYiky7uw Kosaraju
  20. https://www.bilibili.com/video/av83583621?from=search&seid=837966867845986532 Tarjan
  21. https://www.geeksforgeeks.org/articulation-points-or-cut-vertices-in-a-graph/ 割點
  22. https://www.bilibili.com/video/av84615547?p=2 Tarjan求割點和橋
  23. https://www.bilibili.com/video/av64263196?p=34 點雙連通、SPFA(p=13)
  24. https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/ Dijkstra
  25. https://www.youtube.com/watch?v=FtN3BYH2Zes Bellman-Ford
  26. https://blog.csdn.net/HOWARLI/article/details/73824179 Johnson
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章