圖
阿西,CSDN插入圖片實在太頭疼了,以後直接貼github地址吧
圖
- 圖
- 零、基本概念
- 一、圖的遍歷
- 二、並查集UnionFind/Disjoint-set
- 三、最小生成樹(Minimum Spanning Tree)
- 四、拓撲排序Topological Sorting
- 五、強連通分量 SCC
- 六、最短路徑
- 拎出來單獨重點歸納提一下
- 參考資料
零、基本概念
圖的概念:
頂點(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)
-
鄰接列表(adjacency list)
鏈表組成的數組,數組大小等於頂點數。鏈表記錄一個頂點的所有鄰接點。
優點:佔用空間等於O(V+E),增加頂點容易。缺點:查詢邊O(V)
一、圖的遍歷
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森林實際組成部分。
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判斷祖先。
測試用圖:
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)。
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,結束
示例:原圖來自:
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,故不會被選取到。
額,這裏代碼的具體實現,主要是如何檢測最小邊,我寫的代碼中通過遍歷selected數組來選取最小邊,複雜度較高。如圖所示的這種方法,通過不斷覆蓋來判斷一個點連接到mst的最小權重,更優化。
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。另外,拓撲排序不唯一。
應用背景:
- 任務流程圖。例如,學習人工智能前需要先學習數學和編程…先穿褲子,再穿鞋
- 課程安排,預編譯庫,樹的層序遍歷就是一種拓撲排序
4.1 兩種理解
- 一種理解:原圖
-
找到所有頂點的入度,確定入度爲0的頂點爲起點,刪掉該頂點及該頂點所有的出邊
-
重新統計所有頂點的入度,確定入度爲0的頂點爲起點,刪掉該頂點和所有出邊
-
重複上述步驟,若有兩個頂點度爲0,隨便選一個。
- 另一種理解:原圖:
-
任意選擇一個頂點開始DFS遍歷,當一個結點的鄰居結點都被訪問後,將該結點壓入棧中
-
對未訪問的節點進行相同操作,注意結果逆向排列。
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)
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[]:記錄當前已經訪問過,且未被彈出的節點。
假設:
-
開始:選擇一個點開始DFS,每到unvisited的點v就預設dfn[v]=low[v],壓入stack
-
判斷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]),否則會跳過割點回溯到更遠處。用這種寫法更加符合它的定義。)
-
-
當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])
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的邊。
5.2.5.1 割點AP的求法:
粗糙的方法:是一個接一個地刪除所有頂點,並查看頂點的刪除是否會導致圖形斷開,用DFS或BFS,複雜度太高O(V*(V+E))。
牛掰的方法:用Tarjan啊~
因此,從圖中可以看出,v->k,割點v滿足以下條件:
- v是DFS樹的root,並且在DFS樹中至少有兩個孩子
- v不是DFS樹的root,且low[k]>=dfn[v]
DFS樹的葉子不可能是割點
5.2.5.2 橋Bridge 的求法
因此,從圖中可以看出,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 例子
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 例子
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 例子
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 的最短距離爲,單源最短距離可用SPFA求。
設 到 的邊權爲 ,對邊權重新賦值後,令邊權爲
假設uv之間的最短路徑經過,則最短路徑爲
重新賦值後,最短路徑爲
代入新邊權
化簡
結果說明:
因爲u->v,所以,,新的邊權非負,並且從化簡結果可知最短路徑只變化了一個固定的數,這個數可以用Bellman-Ford虛擬點求出。最後再對所有頂點Dijkstra即可。
6.5.2 流程
- 新建一個虛擬的頂點S,令該頂點到所有其他點的距離爲0
- 用優化的Bellman-Ford(SPFA),計算S到其他頂點的最短距離
- 移除虛擬頂點S,重新賦值邊權。
- 對每個頂點Dijkstra+斐波拉契堆,求得最短路徑。
- 所有最短路徑都減掉
6.5.3 新邊權例子
拎出來單獨重點歸納提一下
DFS分爲兩種,假設DFS路徑爲a->b->c->d
- 先訪問當前節點,再遞歸相鄰節點,則訪問順序爲a,b,c,d,輸出順序爲a,b,c,d
- 先遞歸相鄰節點,再訪問當前節點,則訪問順序爲a,b,c,d,輸出順序爲d,c,b,a。這種在Tarjan等算法中很有用
參考資料
- https://www.geeksforgeeks.org/graph-and-its-representations/
- https://www.youtube.com/watch?v=0u78hx-66Xk&list=PLqM7alHXFySEaZgcg7uRYJFBnYMLti-nh&index=2
- https://www.bilibili.com/video/av47042691?from=search&seid=9406491766784608254
- https://www.hackerearth.com/zh/practice/algorithms/graphs/minimum-spanning-tree/tutorial/ MST
- https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-algorithm-greedy-algo-2/ MST
- https://www.geeksforgeeks.org/union-find/ 並查集
- https://www.youtube.com/watch?v=4ZlRH0eK-qQ
- https://www.youtube.com/watch?v=ZtZaR7EcI5Y&list=PLdo5W4Nhv31bK5n8-RIGhvYs8bJbgJFDR&index=5
- https://blog.csdn.net/u014665013/article/details/51351371
- https://www.youtube.com/watch?v=VJnUwsE4fWA 並查集
- https://www.youtube.com/watch?v=wU6udHRIkcc並查集
- https://www.geeksforgeeks.org/kruskals-minimum-spanning-tree-algorithm-greedy-algo-2/ Kruskal
- http://www.codebelief.com/article/2017/04/prim-algorithm-introduction/ Prim
- https://www.cnblogs.com/skywang12345/p/3711507.html Prim
- https://www.geeksforgeeks.org/prims-minimum-spanning-tree-mst-greedy-algo-5/ Prim
- https://www.cnblogs.com/bigsai/p/11489260.html Topological Sorting
- https://www.youtube.com/watch?v=eL-KzMXSXXI Topological Sorting
- https://en.wikipedia.org/wiki/Strongly_connected_component 強連通分量
- https://www.youtube.com/watch?v=RpgcYiky7uw Kosaraju
- https://www.bilibili.com/video/av83583621?from=search&seid=837966867845986532 Tarjan
- https://www.geeksforgeeks.org/articulation-points-or-cut-vertices-in-a-graph/ 割點
- https://www.bilibili.com/video/av84615547?p=2 Tarjan求割點和橋
- https://www.bilibili.com/video/av64263196?p=34 點雙連通、SPFA(p=13)
- https://www.geeksforgeeks.org/dijkstras-shortest-path-algorithm-greedy-algo-7/ Dijkstra
- https://www.youtube.com/watch?v=FtN3BYH2Zes Bellman-Ford
- https://blog.csdn.net/HOWARLI/article/details/73824179 Johnson