演算法基礎<四> 無向圖
連通性問題
目標:編寫一個程式來過濾序列中所有無意義的整數對
程式輸入p,q,如果已知的所有整數都不能說明p,q是相連的,則將這對整數寫到輸出中,如果已知資料已經可以證明,則忽略。
將物件稱為觸電,將整數對稱為連線。將等價類稱為連通分量。簡稱分量
Quck-find演算法
該演算法的目標就是讓所有連通的觸電(索引)對應的值都相同。
public class QuickFindUF { private int[] _id; private int _length; public QuickFindUF(int length) { _length = length; _id = new int[length]; for (int i = 0; i < length; i++) _id[i] = i; } public int Length() { return _length; } public int Find(int p) { validate(p); return _id[p]; } // validate that p is a valid index private void validate(int p) { int n = _id.Length; if (p < 0 || p >= n) { throw new ArgumentException("index " + p + " is not between 0 and " + (n - 1)); } } public bool Connected(int p, int q) { validate(p); validate(q); return _id[p] == _id[q]; } public void Union(int p, int q) { validate(p); validate(q); //將p和q歸併到相同的分量中 int pID = _id[p]; // needed for correctness int qID = _id[q]; // to reduce the number of array accesses // 如果p和q已經在相同的分量之中則不需要採取任何行動 if (pID == qID) return; //將p的分量重新命名為q的名稱 for (int i = 0; i < _id.Length; i++) if (_id[i] == pID) _id[i] = qID; _length--; } }
命題F:在Quick-find演算法中,每次find()呼叫只需要訪問id[]陣列一次。而歸併兩個分量的union()操作訪問陣列的次數在(N+3)到(2N+1)之間。
Quick-unit演算法
還是以觸點作為索引。每個觸電對應的值是另一個觸點(也可以是自己),這稱為連線,find方法,從第一個觸點開始,得到第二個觸點,在得到第4個觸點。
public class QuickUnionUF { private int[] _parent; private int _length; public QuickUnionUF(int length) { _parent = new int[length]; _length = length; for (int i = 0; i < length; i++) { _parent[i] = i; } } public int Length() { return _length; } public int Find(int p) { validate(p); while (p != _parent[p])//找出分量 p = _parent[p]; return p; } // validate that p is a valid index private void validate(int p) { int n = _parent.Length; if (p < 0 || p >= n) { throw new ArgumentException("index " + p + " is not between 0 and " + (n - 1)); } } public bool Connected(int p, int q) { return Find(p) == Find(q); } public void Union(int p, int q) { //將P和Q的根節點統一 int rootP = Find(p); int rootQ = Find(q); if (rootP == rootQ) return; _parent[rootP] = rootQ; _length--; } }
定義:一顆樹的帶下是它的結點的數量,樹中的一個節點的深度是它到根節點的路徑上的連結數。樹的高度是它的所有節點中的最大深度。
命題G:Quick-Union演算法中find()方法訪問陣列的次數為1加上給定觸點對應的節點的深度的兩倍,union和connected訪問陣列的次數為兩次find操作。
最壞情況下還是N^2
加權Quick-union演算法
記錄每棵樹的大小並總是將較小的樹連線到較大的樹上。
public class WeightedQuickUnionUF { /// <summary> /// 父連結陣列(觸點索引) /// </summary> private int[] _parent; /// <summary> /// 各個節點所對應權重的大小 /// </summary> private int[] _size; /// <summary> /// 連通分量的數量 /// </summary> private int _length; public WeightedQuickUnionUF(int length) { _length = length; _parent = new int[length]; _size = new int[length]; for (int i = 0; i < length; i++) { _parent[i] = i; _size[i] = 1;//剛開始所有權重為1 } } public int Length() { return _length; } public bool Connected(int p, int q) { return Find(p) == Find(q); } private void validate(int p) { int n = _parent.Length; if (p < 0 || p >= n) { throw new ArgumentException("index " + p + " is not between 0 and " + (n - 1)); } } public int Find(int p) { validate(p); while (p != _parent[p]) p = _parent[p]; return p; } public void Union(int p, int q) { int rootP = Find(p); int rootQ = Find(q); if (rootP == rootQ) return; //將小樹的根節點連結到大樹的根節點 if (_size[rootP] < _size[rootQ]) { _parent[rootP] = rootQ; _size[rootQ] += _size[rootP]; } else { _parent[rootQ] = rootP;// _size[rootP] += _size[rootQ]; } _length--; } }
命題H:對於N個觸點,加權quick-union演算法構造的森林中的任意節點的深度最多為logN。
推論:對於加權quick-union演算法和N個觸點,在最壞情況下find(),connected()和uniond的成本的增長數量級為logN。
無向圖
無向圖:圖是由一組頂點和一組能夠將連個頂點相連的邊組成的。
一般使用0-(V-1)來表示一張含有V個頂點的圖中的各個頂點。用V-W的記法來表示連線V和W的邊。
由兩種特殊的圖:
- 自環:一條連線一個頂點和自身的邊
- 連線同一對頂點的兩條邊稱為平行邊。
圖的定義和繪出的影象無關
兩個頂點通過同一條邊相連時,稱為這兩個頂點是相鄰的。並稱該連線依附於這兩個頂點。
頂點的度數表示依附於它的邊總數,也就是連線到該頂點的邊的條數。
子圖是一幅圖的所有邊的一個子集(以及它們所依附的所有頂點)組成的圖。
路徑是邊順序連線一系列頂點。
簡單路徑是一條沒有重複頂點的路徑。
環是一條至少含有一條邊且起點和終點相同的路徑。
簡單環:一條(除了起點和終點必須相同之外)不含有重複頂點和邊的環。
長度:路徑和環的長度為其包含的邊數。
連通:如果兩個頂點之間存在一條連線雙方的路徑時,一個頂點和另一個頂點是連通的。路徑用u-v-w-x來表示這條u到x的路徑。
連通圖:從任意一個頂點都存在一條路徑到達另一個任意頂點。
極大連通子圖:一個非連通的圖由若干連通子圖組成。
無環圖:不包含環的圖。
樹:一幅無環連通圖。
森林:互不相連的樹組成的集合。
連通圖的生成樹:是連通圖的一個子集,它包含連通圖中所有頂點並是一顆樹。
圖的生成樹森林:所有連通子圖的生成樹的集合。
樹的辨別條件,一幅含有V個結點的圖G,滿足一點就是樹:
- G有V-1條邊且不含有環;
- G有V-1條邊是連通的;
- G是連通的,但刪除任意一條邊都會使它不再連通
- G是無環圖,但新增任意一條邊都會產生一條環。
- G中任意一對頂點之間僅存一條簡單的路徑。
圖的密度:已經連線的頂點對佔所有可能被連線頂點對的比列。
二分圖:能夠將所有結點分為兩部分的圖,圖的每條邊所連線的兩個頂點都分別屬於不同的部分。
加粗的是一類,不加粗的是另一類
圖的實現
圖的資料表示:
圖的程式碼表示需要滿足以下兩個條件:
- 預留出足夠的空間
- Graph的例項方法實現一定要快
程式碼是實現
- 鄰接矩陣:使用一個V乘V的布林矩陣,需要V^2個布林值的空間,空間是不可能滿足的,造成很多浪費。
- 邊的陣列,使用Edge類,含有兩個int例項變數,不滿足第二個。
- 鄰接標陣列:零點為索引的列表陣列,每個元素都是和該頂點相鄰的列表陣列。
鄰接標的特性:
- 使用的空間和V+E成正比;
- 新增一條邊所需的時間為常數。
- 遍歷頂點V的所有相鄰點所需的時間和V的度數成正比(處理每個相鄰頂點所需的時間為常數)
public class Graph
{
private static readonly string NEWLINE = System.Environment.NewLine;
private readonly int _vertices;
private int _edge;
private LinkedBagNet<int>[] _adj;
public int Vertices => _vertices;
public Graph(int vertices)
{
if (vertices < 1) throw new ArgumentException("this vertices is less than zero");
this._vertices = vertices;
this._edge = 0;
_adj = new LinkedBagNet<int>[vertices];
for (int i = 0; i < vertices; i++)
{
_adj[i] = new LinkedBagNet<int>();
}
}
public Graph(StreamReader stream)
{
if (stream == null) throw new ArgumentException("this stream is null");
try
{
var vertices = int.Parse(stream.ReadLine());
if (vertices < 1) throw new ArgumentException("this vertices value is too small");
this._vertices = vertices;
_adj = new LinkedBagNet<int>[vertices];
for (int i = 0; i < vertices; i++)
{
_adj[i] = new LinkedBagNet<int>();
}
int edge = int.Parse(stream.ReadLine());
if (edge < 1) throw new ArgumentException("this edges is too small");
for (int i = 0; i < edge; i++)
{
string line = stream.ReadLine();
if (!string.IsNullOrEmpty(line) && line.Split(' ').Count() == 2)
{
int v =int.Parse( line.Split(' ')[0]);
int w =int.Parse( line.Split(' ')[1]);
validateVertex(v);
validateVertex(w);
AddEdge(v, w);
}
}
}
catch (Exception ex)
{
throw new ArgumentException("this readstream is not legitimate");
}
}
public Graph(Graph graph)
{
this._vertices = graph._vertices;
this._edge = graph._edge;
if(_vertices<1) throw new ArgumentException("this vertices is too small");
_adj=new LinkedBagNet<int>[_vertices];
for (int i = 0; i < _vertices; i++)
{
_adj[i]=new LinkedBagNet<int>();
}
for (int i = 0; i < _vertices; i++)
{
foreach (var value in graph._adj[i])
{
_adj[i].Add(value);
}
}
}
private void validateVertex(int v)
{
if (v < 0 || v > _vertices) throw new ArgumentException("this v does not in range");
}
public void AddEdge(int v, int w)
{
validateVertex(v);
validateVertex(w);
_edge++;
_adj[v].Add(w);
_adj[w].Add(v);
}
public int Degree(int v)
{
validateVertex(v);
return _adj[v].Length;
}
}
圖生成器
public static class GraphGenerator
{
private class Edge : IComparable<Edge>
{
private int _v;
private int _w;
public Edge(int v, int w)
{
if (v < w)
{
this._v = v;
this._w = w;
}
else
{
this._v = w;
this._w = v;
}
}
public int CompareTo(Edge other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
var vComparison = _v.CompareTo(other._v);
if (vComparison != 0) return vComparison;
var wComparison = _w.CompareTo(other._w);
if (wComparison != 0) return wComparison;
return 0;
}
}
/// <summary>
/// 生成簡單圖
/// </summary>
/// <param name="V">頂點數量</param>
/// <param name="E">邊的數量</param>
/// <returns></returns>
public static Graph Simple(int V, int E)
{
if (E > (long)V * (V - 1) / 2) throw new ArgumentException("Too many edges");
if (E < 0) throw new ArgumentException("Too few edges");
Graph G = new Graph(V);
List<Edge> set = new List<Edge>();
var random=new Random();
while (G.Edge < E)
{
int v = random.Next(V);
int w = random.Next(V);
Edge e = new Edge(v, w);
if ((v != w) && !set.Contains(e))
{
set.Add(e);
G.AddEdge(v, w);
}
}
return G;
}
/// <summary>
/// 生成簡單圖
/// </summary>
/// <param name="V">頂點的數量</param>
/// <param name="p">選擇邊的概率</param>
/// <returns></returns>
public static Graph Simple(int V, double p)
{
if (p < 0.0 || p > 1.0)
throw new ArgumentException("Probability must be between 0 and 1");
Graph G = new Graph(V);
for (int v = 0; v < V; v++)
for (int w = v + 1; w < V; w++)
if (new Random().NextDouble()<p)
G.AddEdge(v, w);
return G;
}
public static Graph Complete(int V)
{
return Simple(V, 1.0);
}
/// <summary>
/// 在V1和V2頂點上返回完整的二分圖
/// </summary>
/// <param name="V1"></param>
/// <param name="V2"></param>
/// <returns></returns>
public static Graph CompleteBipartite(int V1, int V2)
{
return Bipartite(V1, V2, V1 * V2);
}
/// <summary>
///
/// </summary>
/// <param name="V1"></param>
/// <param name="V2"></param>
/// <param name="E">邊數</param>
/// <returns></returns>
public static Graph Bipartite(int V1, int V2, int E)
{
if (E > (long)V1 * V2) throw new ArgumentException("Too many edges");
if (E < 0) throw new ArgumentException("Too few edges");
Graph G = new Graph(V1 + V2);
int[] vertices = new int[V1 + V2];
for (int i = 0; i < V1 + V2; i++)
vertices[i] = i;
vertices.Shuffle();
List<Edge> set = new List<Edge>();
Random randon=new Random();
while (G.Edge < E)
{
int i = randon.Next(V1);
int j = V1 + randon.Next(V2);
Edge e = new Edge(vertices[i], vertices[j]);
if (!set.Contains(e))
{
set.Add(e);
G.AddEdge(vertices[i], vertices[j]);
}
}
return G;
}
public static Graph Bipartite(int V1, int V2, double p)
{
if (p < 0.0 || p > 1.0)
throw new ArgumentException("Probability must be between 0 and 1");
int[] vertices = new int[V1 + V2];
for (int i = 0; i < V1 + V2; i++)
vertices[i] = i;
vertices.Shuffle();
Graph G = new Graph(V1 + V2);
for (int i = 0; i < V1; i++)
for (int j = 0; j < V2; j++)
if (new Random().NextDouble() < p)
G.AddEdge(vertices[i], vertices[V1 + j]);
return G;
}
/// <summary>
///
/// </summary>
/// <param name="V"></param>
/// <returns></returns>
public static Graph Path(int V)
{
Graph G = new Graph(V);
int[] vertices = new int[V];
for (int i = 0; i < V; i++)
vertices[i] = i;
vertices.Shuffle();
for (int i = 0; i < V - 1; i++)
{
G.AddEdge(vertices[i], vertices[i + 1]);
}
return G;
}
/// <summary>
/// 樹圖
/// </summary>
/// <param name="V">頂點數</param>
/// <returns></returns>
public static Graph BinaryTree(int V)
{
Graph G = new Graph(V);
int[] vertices = new int[V];
for (int i = 0; i < V; i++)
vertices[i] = i;
vertices.Shuffle();
for (int i = 1; i < V; i++)
{
G.AddEdge(vertices[i], vertices[(i - 1) / 2]);
}
return G;
}
/// <summary>
/// 環圖
/// </summary>
/// <param name="V"></param>
/// <returns></returns>
public static Graph Cycle(int V)
{
Graph G = new Graph(V);
int[] vertices = new int[V];
for (int i = 0; i < V; i++)
vertices[i] = i;
vertices.Shuffle();
for (int i = 0; i < V - 1; i++)
{
G.AddEdge(vertices[i], vertices[i + 1]);
}
G.AddEdge(vertices[V - 1], vertices[0]);
return G;
}
/// <summary>
///
/// </summary>
/// <param name="V"></param>
/// <param name="E"></param>
/// <returns></returns>
public static Graph EulerianCycle(int V, int E)
{
if (E <= 0)
throw new ArgumentException("An Eulerian cycle must have at least one edge");
if (V <= 0)
throw new ArgumentException("An Eulerian cycle must have at least one vertex");
Graph G = new Graph(V);
int[] vertices = new int[E];
Random random=new Random();
for (int i = 0; i < E; i++)
vertices[i] =random.Next(V);
for (int i = 0; i < E - 1; i++)
{
G.AddEdge(vertices[i], vertices[i + 1]);
}
G.AddEdge(vertices[E - 1], vertices[0]);
return G;
}
public static Graph EulerianPath(int V, int E)
{
if (E < 0)
throw new ArgumentException("negative number of edges");
if (V <= 0)
throw new ArgumentException("An Eulerian path must have at least one vertex");
Graph G = new Graph(V);
int[] vertices = new int[E + 1];
Random random=new Random();
for (int i = 0; i < E + 1; i++)
vertices[i] = random.Next(V);
for (int i = 0; i < E; i++)
{
G.AddEdge(vertices[i], vertices[i + 1]);
}
return G;
}
public static Graph Wheel(int V)
{
if (V <= 1) throw new ArgumentException("Number of vertices must be at least 2");
Graph G = new Graph(V);
int[] vertices = new int[V];
for (int i = 0; i < V; i++)
vertices[i] = i;
vertices.Shuffle();
// simple cycle on V-1 vertices
for (int i = 1; i < V - 1; i++)
{
G.AddEdge(vertices[i], vertices[i + 1]);
}
G.AddEdge(vertices[V - 1], vertices[1]);
// connect vertices[0] to every vertex on cycle
for (int i = 1; i < V; i++)
{
G.AddEdge(vertices[0], vertices[i]);
}
return G;
}
public static Graph Star(int V)
{
if (V <= 0) throw new ArgumentException("Number of vertices must be at least 1");
Graph G = new Graph(V);
int[] vertices = new int[V];
for (int i = 0; i < V; i++)
vertices[i] = i;
vertices.Shuffle();
// connect vertices[0] to every other vertex
for (int i = 1; i < V; i++)
{
G.AddEdge(vertices[0], vertices[i]);
}
return G;
}
public static Graph Regular(int V, int k)
{
if (V * k % 2 != 0) throw new ArgumentException("Number of vertices * k must be even");
Graph G = new Graph(V);
// create k copies of each vertex
int[] vertices = new int[V * k];
for (int v = 0; v < V; v++)
{
for (int j = 0; j < k; j++)
{
vertices[v + V * j] = v;
}
}
// pick a random perfect matching
vertices.Shuffle();
for (int i = 0; i < V * k / 2; i++)
{
G.AddEdge(vertices[2 * i], vertices[2 * i + 1]);
}
return G;
}
public static Graph Tree(int V)
{
Graph G = new Graph(V);
// special case
if (V == 1) return G;
// Cayley's theorem: there are V^(V-2) labeled trees on V vertices
// Prufer sequence: sequence of V-2 values between 0 and V-1
// Prufer's proof of Cayley's theorem: Prufer sequences are in 1-1
// with labeled trees on V vertices
int[] prufer = new int[V - 2];
Random random=new Random();
for (int i = 0; i < V - 2; i++)
prufer[i] = random.Next(V);
// degree of vertex v = 1 + number of times it appers in Prufer sequence
int[] degree = new int[V];
for (int v = 0; v < V; v++)
degree[v] = 1;
for (int i = 0; i < V - 2; i++)
degree[prufer[i]]++;
// pq contains all vertices of degree 1
MinPQNet<int> pq = new MinPQNet<int>();
for (int v = 0; v < V; v++)
if (degree[v] == 1) pq.Insert(v);
// repeatedly delMin() degree 1 vertex that has the minimum index
for (int i = 0; i < V - 2; i++)
{
int v = pq.DeleteMin();
G.AddEdge(v, prufer[i]);
degree[v]--;
degree[prufer[i]]--;
if (degree[prufer[i]] == 1) pq.Insert(prufer[i]);
}
G.AddEdge(pq.DeleteMin(), pq.DeleteMin());
return G;
}
public static int[] Shuffle(this int[] stack)
{
var random=new Random();
for (int i = 0; i < stack.Length; i++)
{
int temindex = random.Next(stack.Length);
if (i != temindex)
{
int tem = stack[i];
stack[i] = stack[temindex];
stack[temindex] = tem;
}
}
return stack;
}
public static void TestFunction(int V,int E)
{
int V1 = V / 2;
int V2 = V - V1;
Console.WriteLine("complete graph");
Console.WriteLine(Complete(V));
Console.WriteLine("simple");
Console.WriteLine(Simple(V, E));
Console.WriteLine("Erdos-Renyi");
double p = (double)E / (V * (V - 1) / 2.0);
Console.WriteLine(Simple(V, p));
Console.WriteLine("complete bipartite");
Console.WriteLine(CompleteBipartite(V1, V2));
Console.WriteLine("bipartite");
Console.WriteLine(Bipartite(V1, V2, E));
Console.WriteLine("Erdos Renyi bipartite");
double q = (double)E / (V1 * V2);
Console.WriteLine(Bipartite(V1, V2, q));
Console.WriteLine("path");
Console.WriteLine(Path(V));
Console.WriteLine("cycle");
Console.WriteLine(Cycle(V));
Console.WriteLine("binary tree");
Console.WriteLine(BinaryTree(V));
Console.WriteLine("tree");
Console.WriteLine(Tree(V));
Console.WriteLine("4-regular");
Console.WriteLine(Regular(V, 4));
Console.WriteLine("star");
Console.WriteLine(Star(V));
Console.WriteLine("wheel");
Console.WriteLine(Wheel(V));
}
}
圖的處理演算法
圖的表示和實現是分開的
Search(Graph graph, int s):找到和起點S連通的所有頂點。
Marked(int v):v和s是連通的嗎
從圖中起點開始沿著路徑到達其他頂點並標記每個路過的頂點。
深度優先演算法(DFS)
查詢所有節點
Tremaux搜尋:
- 選擇一條沒有標記過的通道,在走過的路上鋪一條繩子
- 標記所有第一次路過的路口和通道
- 當回退到路口已沒有可走的通道時繼續回退。
Tremaux可以保證找到一條路,但不能保證完全探索整張圖。
深度優先搜尋:利用遞迴遍歷所有的邊和頂點,在訪問一個頂點時,將它標記為已訪問,遞迴地訪問它所有沒有被標記過的鄰居頂點。
命題A:深度優先搜尋標記與起點連通的所有頂點所需的時間和頂點的度數之和成正比。
將圖會化成單向通道,當V-W時,要麼遞迴呼叫(w沒有被標記過),要麼跳過這條邊(w已經被標記過),第二次從w-v遇到這條邊,會忽略它,因為另一端v肯定被訪問過。
深度搜索每條邊都被訪問兩次,第一次訪問標記,第二次訪問會發現這個頂點被標記過。
深度優先搜尋示例
0鄰接表2,1,5,優先訪問2,
2鄰接表0,1,3,4,0標記過,然後訪問1,
1鄰接表0,2,都標記過,訪問3的鄰接標
3鄰接表,5,4,2,先訪問5,再訪問4,2被標記過,
這邊應該再檢查2鄰接表中的4,所有點都訪問過。
需要解決的問題:兩個給定的頂點是否連通?有多少個連通子圖?
public class DepthFirstSearch
{
private bool[] _marked;//s-v的路徑記錄
private int _count;//連線到s的頂點數量
public int Count => _count;
public DepthFirstSearch(Graph graph, int s)
{
_marked = new bool[graph.Vertices];
validateVertex(s);
dfs(graph,s);
}
private void dfs(Graph G, int v)
{
_count++;
_marked[v] = true;//訪問過的節點為true
foreach (int w in G.Adj[v])
{
if (!_marked[w])
{
dfs(G, w);//遞迴呼叫
}
}
}
private void validateVertex(int v)
{
int V = _marked.Length;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
public bool Marked(int v)
{
validateVertex(v);
return _marked[v];
}
}
測試
[TestMethod()]
public void DepthFirstTest()
{
var data = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data\\tinyG.txt");
Console.WriteLine(data);
using (StreamReader stream = new StreamReader(data))
{
AlgorithmEngine.Graph.Graph graph = new AlgorithmEngine.Graph.Graph(stream);
Console.WriteLine($"Graph vertices:{graph.Vertices}");
DepthFirstSearch search = new DepthFirstSearch(graph, 1);
for (int v = 0; v < graph.Vertices; v++)
{
if(search.Marked(v))
Console.Write(v+" ");
}
}
}
//Graph vertices:13
//0 1 2 3 4 5 6
深度搜索:無遞迴
/// <summary>
/// 深度優先演算法,無遞迴
/// </summary>
public class NonrecursiveDFS
{
private bool[] _marked;
public NonrecursiveDFS(Graph G, int s)
{
_marked = new bool[G.Vertices];
validateVertex(s);
// 儲存鄰接表
IEnumerator[] adj = new IEnumerator<int>[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
adj[v] = G.Adj.GetEnumerator();
// 需要一個額外的棧來存放節點
Stack<int> stack = new Stack<int>();
_marked[s] = true;
stack.Push(s);
while (stack.Any())
{
int v = stack.Peek();
if (adj[v].MoveNext())
{
int w =(int) adj[v].Current;
if (!_marked[w])
{
//標記節點
_marked[w] = true;
stack.Push(w);//壓棧
}
}
else
{
stack.Pop();//訪問完該節點的所有鄰接點,出棧
}
}
}
public bool Marked(int v)
{
validateVertex(v);
return _marked[v];
}
private void validateVertex(int v)
{
int V = _marked.Length;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
深度優先演算法是查詢所有的節點。
尋找所有路徑
通過新增例項變數edgeTo()整型陣列來模仿Tremaux搜尋中繩子的作用。它用來記錄每個頂點到起點的路徑。
public class DepthFirstPaths
{
private bool[] _marked;
private int[] _edgeTo;
/// <summary>
/// start
/// </summary>
private readonly int _start;
public DepthFirstPaths(Graph G, int s)
{
this._start = s;
_edgeTo = new int[G.Vertices];
_marked = new bool[G.Vertices];
validateVertex(s);
dfs(G, s);
}
private void dfs(Graph G, int v)
{
_marked[v] = true;
foreach(int w in G.Adj[v])
{
if (!_marked[w])
{
_edgeTo[w] = v;//通到w是v
dfs(G, w);
}
}
}
public bool HasPathTo(int v)
{
validateVertex(v);
return _marked[v];
}
public IEnumerable<int> PathTo(int v)
{
validateVertex(v);
if (!HasPathTo(v)) return null;
Stack<int> path = new Stack<int>();//通過棧先進後出
for (int x = v; x != _start; x = _edgeTo[x])
path.Push(x);
path.Push(_start);
return path;
}
private void validateVertex(int v)
{
int V = _marked.Length;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
測試程式碼:
[TestMethod()]
public void DepthFirstPathsTest()
{
var data = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data\\tinyG.txt");
Console.WriteLine(data);
using (StreamReader stream = new StreamReader(data))
{
AlgorithmEngine.Graph.Graph graph = new AlgorithmEngine.Graph.Graph(stream);
Console.WriteLine($"Graph vertices:{graph.Vertices}");
DepthFirstPaths dfs = new DepthFirstPaths(graph, 1);
for (int v = 0; v < graph.Vertices; v++)
{
if (dfs.HasPathTo(v))
{
Console.Write($"1 to {v}: ");
foreach (int x in dfs.PathTo(v))
{
if (x == 1) Console.Write(x);
else Console.Write("-" + x);
}
Console.Write(System.Environment.NewLine);
}
else
{
Console.Write($"1 to {v}: not connected\n");
}
}
}
//Graph vertices:13
//1 to 0: 1 - 0
//1 to 1: 1
//1 to 2: 1 - 0 - 2
//1 to 3: 1 - 0 - 6 - 4 - 5 - 3
//1 to 4: 1 - 0 - 6 - 4
//1 to 5: 1 - 0 - 6 - 4 - 5
//1 to 6: 1 - 0 - 6
//1 to 7: not connected
//1 to 8: not connected
//1 to 9: not connected
//1 to 10: not connected
//1 to 11: not connected
//1 to 12: not connected
}
edgeTo的軌跡
命題A:使用深度優先搜尋得到從給定起點到任意標記頂點的路徑所需的時間與路徑的長度成正比。
連通分量
找出圖中所有的連通分量,與……連通。
public class CC
{
private bool[] _marked;
private int[] _id; // id[v] ,v所在連通分量的識別符號
private int _count; // 連通分量的數量
public CC(Graph G)
{
_marked = new bool[G.Vertices];
_id = new int[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
{
if (!_marked[v])
{
dfs(G, v);
_count++;//從不同頂點開始遍歷,遇到沒有標記過的頂點,連通分量加一
}
}
}
private void dfs(Graph G, int v)
{
_marked[v] = true;
_id[v] = _count;
foreach (int w in G.Adj[v])//這邊使用的是深度優先
{
if (!_marked[w])
{
dfs(G, w);
}
}
}
/// <summary>
/// v所在連通分量表示符
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
public int Id(int v)
{
validateVertex(v);
return _id[v];
}
public int Count()
{
return _count;
}
public bool Connected(int v, int w)
{
validateVertex(v);
validateVertex(w);
return Id(v) == Id(w);
}
public bool AreConnected(int v, int w)
{
validateVertex(v);
validateVertex(w);
return Id(v) == Id(w);
}
private void validateVertex(int v)
{
int V = _marked.Length;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
測試程式碼
[TestMethod()]
public void CCTest()
{
var data = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data\\tinyG.txt");
Console.WriteLine(data);
using (StreamReader stream = new StreamReader(data))
{
AlgorithmEngine.Graph.Graph G = new AlgorithmEngine.Graph.Graph(stream);
CC cc = new CC(G);
// number of connected components
int m = cc.Count();
Console.WriteLine(m + " components ");
// compute list of vertices in each connected component
Queue<int>[] components = new Queue<int>[m];
for (int i = 0; i < m; i++)
{
components[i] = new Queue<int>();
}
for (int v = 0; v < G.Vertices; v++)
{
components[cc.Id(v)].Enqueue(v);
}
// print results
for (int i = 0; i < m; i++)
{
foreach (int v in components[i])
{
Console.Write(v + " ");
}
Console.Write(System.Environment.NewLine);
}
//3 components
//0 1 2 3 4 5 6
//7 8
//9 10 11 12
}
}
命題C:深度優先搜尋的預處理使用的時間和空間與V+E成正比且可以在常數時間內處理關於圖的連通性查詢。
二分圖
二分圖:能夠將所有結點分為兩部分的圖,圖的每條邊所連線的兩個頂點都分別屬於不同的部分。
/// <summary>
/// 二分圖:能夠將所有結點分為兩部分的圖,圖的每條邊所連線的兩個頂點都分別屬於不同的部分。
/// 深度優先搜尋
/// </summary>
public class Bipartite
{
/// <summary>
/// 是否是二分圖
/// </summary>
private bool _isBipartite;
/// <summary>
/// 用來區分節點顏色
/// </summary>
private bool[] _color;
/// <summary>
/// 是否被訪問過
/// </summary>
private bool[] _marked;
private int[] _edgeTo;
/// <summary>
/// 用來儲存一次奇迴圈
/// </summary>
private Stack<int> _cycle;
public bool IsBipartite => _isBipartite;
public Bipartite(Graph G)
{
_isBipartite = true;
_color = new bool[G.Vertices];
_marked = new bool[G.Vertices];
_edgeTo = new int[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
{
if (!_marked[v])
{
dfs(G, v);
}
}
}
private void dfs(Graph G, int v)
{
_marked[v] = true;
foreach (int w in G.Adj[v])
{
// short circuit if odd-length cycle found
if (_cycle != null) return;
// found uncolored vertex, so recur
if (!_marked[w])
{
_edgeTo[w] = v;
_color[w] = !_color[v];//顏色與頂點顏色想反
dfs(G, w);//深度優先演算法
}
else if (_color[w] == _color[v])//如果被訪問過,頂點顏色又一樣,那麼肯定不是二分圖
{
_isBipartite = false;
_cycle = new Stack<int>();//記錄連通分量
_cycle.Push(w);
for (int x = v; x != w; x = _edgeTo[x])
{
_cycle.Push(x);
}
_cycle.Push(w);
}
}
}
/// <summary>
/// 節點顏色
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
public bool Color(int v)
{
validateVertex(v);
if (!_isBipartite)
throw new ArgumentException("graph is not bipartite");
return _color[v];
}
public IEnumerable<int> oddCycle()
{
return _cycle;
}
private bool check(Graph G)
{
// graph is bipartite
if (_isBipartite)
{
for (int v = 0; v < G.Vertices; v++)
{
foreach (int w in G.Adj[v])
{
if (_color[v] == _color[w])
{
Console.Write($"edge {v}-{w} with {v} and {w} in same side of bipartition\n");
return false;
}
}
}
}
// graph has an odd-length cycle
else
{
// verify cycle
int first = -1, last = -1;
foreach (int v in oddCycle())
{
if (first == -1) first = v;
last = v;
}
if (first != last)
{
Console.Write($"cycle begins with {first} and ends with {last}\n", first, last);
return false;
}
}
return true;
}
// throw an IllegalArgumentException unless {@code 0 <= v < V}
private void validateVertex(int v)
{
int V = _marked.Length;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
測試
[TestMethod()]
public void BipartiteTest()
{
AlgorithmEngine.Graph.Graph G = GraphGenerator.Bipartite(6, 6, 10);
//Random random=new Random();
//for (int i = 0; i < 10; i++)
//{
// int v = random.Next(10);
// int w = random.Next(10);
// G.AddEdge(v, w);
//}
Bipartite b=new Bipartite(G);
if (b.IsBipartite)
{
Console.Write("Graph is bipartite: ");
for (int v = 0; v < G.Vertices; v++)
{
Console.Write(v + ": " + b.Color(v)+"; ");
}
}
else
{
Console.Write("Graph has an odd-length cycle: ");
foreach (int x in b.oddCycle())
{
Console.Write(x + " ");
}
Console.WriteLine();
}
}
無環圖
public class Cycle
{
private bool[] _marked;
private int[] _edgeTo;
private Stack<int> _cycle;
public Cycle(Graph G)
{
if (hasSelfLoop(G)) return;
if (hasParallelEdges(G)) return;
_marked = new bool[G.Vertices];
_edgeTo = new int[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
if (!_marked[v])
dfs(G, -1, v);
}
/// <summary>
/// 檢查自環
/// </summary>
/// <param name="G"></param>
/// <returns></returns>
private bool hasSelfLoop(Graph G)
{
for (int v = 0; v < G.Vertices; v++)
{
foreach (int w in G.Adj[v])
{
if (v == w)
{
_cycle = new Stack<int>();
_cycle.Push(v);
_cycle.Push(v);
return true;
}
}
}
return false;
}
/// <summary>
/// 檢查平行邊
/// </summary>
/// <param name="G"></param>
/// <returns></returns>
private bool hasParallelEdges(Graph G)
{
_marked = new bool[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
{
//平行邊就是鄰接矩陣中有相同項
foreach (int w in G.Adj[v])
{
if (_marked[w])
{
_cycle = new Stack<int>();
_cycle.Push(v);
_cycle.Push(w);
_cycle.Push(v);
return true;
}
_marked[w] = true;
}
foreach (int w in G.Adj[v])
{
_marked[w] = false;
}
}
return false;
}
public bool HasCycle()
{
return _cycle != null;
}
public IEnumerable<int> GetCycle()
{
return _cycle;
}
private void dfs(Graph G, int u, int v)
{
_marked[v] = true;
foreach (int w in G.Adj[v])
{
if (_cycle != null) return;
if (!_marked[w])
{
_edgeTo[w] = v;
dfs(G, v, w);
}
else if (w != u)//如果周圍的項有重複並且不是自環
{
_cycle = new Stack<int>();
for (int x = v; x != w; x = _edgeTo[x])
{
_cycle.Push(x);
}
_cycle.Push(w);
_cycle.Push(v);
}
}
}
}
測試:
[TestMethod()]
public void CycleTest()
{
AlgorithmEngine.Graph.Graph G = GraphGenerator.Cycle(15);
Cycle finder=new Cycle(G);
if (finder.HasCycle())
{
foreach (int v in finder.GetCycle())
{
Console.Write(v + " ");
}
Console.WriteLine();
}
else
{
Console.WriteLine("Graph is acyclic");
}
}
尤拉環
尤拉環:恰好包含了所有的邊且沒用重複的環,尤拉環就是尤拉路徑起點和終點一樣。
/// <summary>
/// 尤拉環:恰好包含所有的邊且沒有重複的環
/// </summary>
public class EulerianCycle
{
private Stack<int> _cycle = new Stack<int>();
/// <summary>
/// 無狀態邊
/// 指示是否被使用
/// </summary>
private class Edge
{
public int V;
public int W;
public bool IsUsed;
public Edge(int v, int w)
{
this.V = v;
this.W = w;
IsUsed = false;
}
/// <summary>
/// 返回邊的另一個頂點
/// </summary>
/// <param name="vertex"></param>
/// <returns></returns>
public int Other(int vertex)
{
if (vertex == V) return W;
else if (vertex == W) return V;
else throw new ArgumentException("Illegal endpoint");
}
}
public EulerianCycle(Graph G)
{
// 至少含有一個邊
if (G.Edge == 0) return;
// 所有的頂點都具有偶數度
// 否則會找到的尤拉路徑肯能不是環
for (int v = 0; v < G.Vertices; v++)
if (G.Degree(v) % 2 != 0)
return;
// 建立鄰接表本地副本,一次迭代一個頂點
Queue<Edge>[] adj = new Queue<Edge>[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
adj[v] = new Queue<Edge>();
for (int v = 0; v < G.Vertices; v++)
{
int selfLoops = 0;
foreach (int w in G.Adj[v])
{
//小心自環
if (v == w)
{
if (selfLoops % 2 == 0)
{
Edge e = new Edge(v, w);
adj[v].Enqueue(e);//頂點對應的邊
adj[w].Enqueue(e);
}
selfLoops++;//自環統計
}
else if (v < w)
{
Edge e = new Edge(v, w);
adj[v].Enqueue(e);
adj[w].Enqueue(e);
}
}
}
// 非單獨的頂點,確定起始點
int s = nonIsolatedVertex(G);
Stack<int> stack = new Stack<int>();
stack.Push(s);
//使用深度搜索
_cycle = new Stack<int>();
while (stack.Any())
{
int v = stack.Pop();
while (adj[v].Any())
{
Edge edge = adj[v].Dequeue();
if (edge.IsUsed) continue;
edge.IsUsed = true;
stack.Push(v);
v = edge.Other(v);
}
_cycle.Push(v);
}
// 確定所有的邊都是用
if (_cycle.Count != G.Vertices + 1)
_cycle = null;
}
/// <summary>
/// 返回尤拉環的頂點
/// </summary>
/// <returns></returns>
public IEnumerable<int> GetCycle()
{
return _cycle;
}
/// <summary>
/// 是否存在尤拉環
/// </summary>
/// <returns></returns>
public bool hasEulerianCycle()
{
return _cycle != null;
}
/// <summary>
/// 返回單獨的頂點,如果不是單獨的頂點返回該頂點
/// </summary>
/// <param name="G"></param>
/// <returns></returns>
private static int nonIsolatedVertex(Graph G)
{
for (int v = 0; v < G.Vertices; v++)
if (G.Degree(v) > 0)
return v;
return -1;
}
private static bool satisfiesNecessaryAndSufficientConditions(Graph G)
{
// Condition 0: at least 1 edge
if (G.Edge == 0) return false;
// Condition 1: degree(v) is even for every vertex
for (int v = 0; v < G.Vertices; v++)
if (G.Degree(v) % 2 != 0)
return false;
// Condition 2: graph is connected, ignoring isolated vertices
int s = nonIsolatedVertex(G);
BreadthFirstPaths bfs = new BreadthFirstPaths(G, s);
for (int v = 0; v < G.Vertices; v++)
if (G.Degree(v) > 0 && !bfs.HasPathTo(v))
return false;
return true;
}
private bool certifySolution(Graph G)
{
// internal consistency check
if (hasEulerianCycle() == (GetCycle() == null)) return false;
// hashEulerianCycle() returns correct value
if (hasEulerianCycle() != satisfiesNecessaryAndSufficientConditions(G)) return false;
// nothing else to check if no Eulerian cycle
if (_cycle == null) return true;
// check that cycle() uses correct number of edges
if (_cycle.Count() != G.Edge + 1) return false;
// check that cycle() is a cycle of G
// TODO
// check that first and last vertices in cycle() are the same
int first = -1, last = -1;
foreach (int v in GetCycle())
{
if (first == -1) first = v;
last = v;
}
if (first != last) return false;
return true;
}
}
測試
[TestMethod()]
public void EulerianCycleTest()
{
AlgorithmEngine.Graph.Graph G = GraphGenerator.EulerianCycle(5, 5);
EulerianCycle euler = new EulerianCycle(G);
if (euler.hasEulerianCycle())
{
foreach (int v in euler.GetCycle())
{
Console.Write(v + " ");
}
Console.WriteLine();
}
else
{
Console.Write("none");
}
Console.WriteLine();
//1 2 3 3 3 1
}
尤拉路徑
尤拉路徑:一個路徑包括每個邊恰好一次
public class EulerianPath
{
private Stack<int> _path = null;
private class Edge
{
public int V;
public int W;
public bool IsUsed;
public Edge(int v, int w)
{
this.V = v;
this.W = w;
IsUsed = false;
}
/// <summary>
/// 返回邊的另一個頂點
/// </summary>
/// <param name="vertex"></param>
/// <returns></returns>
public int Other(int vertex)
{
if (vertex == V) return W;
else if (vertex == W) return V;
else throw new ArgumentException("Illegal endpoint");
}
}
public EulerianPath(Graph G)
{
// 查詢尤拉路徑的起點
// 如果有頂點的度數是奇數,那麼不符合尤拉路徑
// 每個頂點至少有偶數度
int oddDegreeVertices = 0;
int s = nonIsolatedVertex(G);
for (int v = 0; v < G.Vertices; v++)
{
if (G.Degree(v) % 2 != 0)
{
oddDegreeVertices++;
s = v;
}
}
// 一個圖的奇數頂點最多隻有2個起點和終點。
if (oddDegreeVertices > 2) return;
// 一種特殊的沒有邊的圖
if (s == -1) s = 0;
// 建立本地鄰接表
Queue<Edge>[] adj = new Queue<Edge>[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
adj[v] = new Queue<Edge>();
for (int v = 0; v < G.Vertices; v++)
{
int selfLoops = 0;
foreach (int w in G.Adj[v])
{
// 自環
if (v == w)
{
if (selfLoops % 2 == 0)
{
Edge e = new Edge(v, w);
adj[v].Enqueue(e);
adj[w].Enqueue(e);
}
selfLoops++;
}
else if (v < w)
{
Edge e = new Edge(v, w);
adj[v].Enqueue(e);
adj[w].Enqueue(e);
}
}
}
Stack<int> stack = new Stack<int>();
stack.Push(s);
// 深度搜索
_path = new Stack<int>();
while (stack.Any())
{
int v = stack.Pop();
while (adj[v].Any())//所有的邊
{
Edge edge = adj[v].Dequeue();//使用過的邊就彈出
if (edge.IsUsed) continue;
edge.IsUsed = true;
stack.Push(v);
v = edge.Other(v);//探索邊的另一個頂點
}
// 記錄路徑
_path.Push(v);
}
// 如果所有的邊都用了
if (_path.Count != G.Edge + 1)
_path = null;
}
public IEnumerable<int> Path()
{
return _path;
}
public bool HasEulerianPath()
{
return _path != null;
}
private static int nonIsolatedVertex(Graph G)
{
for (int v = 0; v < G.Vertices; v++)
if (G.Degree(v) > 0)
return v;
return -1;
}
/// <summary>
/// 必要和充分條件
/// </summary>
/// <param name="G"></param>
/// <returns></returns>
private static bool satisfiesNecessaryAndSufficientConditions(Graph G)
{
if (G.Edge == 0) return true;
// Condition 1: degree(v) is even except for possibly two
int oddDegreeVertices = 0;
for (int v = 0; v < G.Vertices; v++)
if (G.Degree(v) % 2 != 0)
oddDegreeVertices++;
if (oddDegreeVertices > 2) return false;
// Condition 2: graph is connected, ignoring isolated vertices
int s = nonIsolatedVertex(G);
BreadthFirstPaths bfs = new BreadthFirstPaths(G, s);
for (int v = 0; v < G.Vertices; v++)
if (G.Degree(v) > 0 && !bfs.HasPathTo(v))
return false;
return true;
}
private bool certifySolution(Graph G)
{
// internal consistency check
if (HasEulerianPath() == (_path == null)) return false;
// hashEulerianPath() returns correct value
if (HasEulerianPath() != satisfiesNecessaryAndSufficientConditions(G)) return false;
// nothing else to check if no Eulerian path
if (_path == null) return true;
// check that path() uses correct number of edges
if (_path.Count != G.Edge + 1) return false;
// check that path() is a path in G
// TODO
return true;
}
}
測試
[TestMethod()]
public void EulerianPathTest()
{
AlgorithmEngine.Graph.Graph G = GraphGenerator.EulerianCycle(5, 5);
EulerianPath euler = new EulerianPath(G);
if (euler.HasEulerianPath())
{
foreach (int v in euler.Path())
{
Console.Write(v + " ");
}
Console.WriteLine();
}
else
{
Console.Write("none");
}
Console.WriteLine();
//2 4 4 3 3 2
}
廣度優先搜尋(BFS)
最短路徑
廣度優先搜尋(BFS)解決了單點最短路徑的問題。
深度搜索就像一個人在走迷宮,廣度搜索就像一組人在走迷宮,每個人都有繩子,當兩個人相遇的時候,會合並使用較短繩子的那個人。
在深度搜索中,使用了LIFO(後進先出)棧來描述走過的路徑。
在廣度搜索中,按照距離與起點的距離的順序來遍歷所有頂點,使用FIFO先進先出佇列來替換LIFO後進先出佇列。
public class BreadthFirstPaths
{
private static readonly int INFINITY = int.MaxValue;
private bool[] _marked; //到達該頂點的最短路徑是否已知
private int[] _edgeTo; // edgeTo[v] = s 表示指向頂點v的頂點是s,也表示邊s-V
private int[] _distTo; // distTo[v] = s 表示s到達到v的邊的數量
public BreadthFirstPaths(Graph G, int s)
{
_marked = new bool[G.Vertices];
_distTo = new int[G.Vertices];
_edgeTo = new int[G.Vertices];
validateVertex(s);
bfs(G, s);
}
public BreadthFirstPaths(Graph G, IEnumerable<int> sources)
{
_marked = new bool[G.Vertices];
_distTo = new int[G.Vertices];
_edgeTo = new int[G.Vertices];
for (int v = 0; v < G.Vertices; v++)
_distTo[v] = INFINITY;
validateVertices(sources);
bfs(G, sources);
}
private void bfs(Graph G, int s)
{
Queue<int> q = new Queue<int>();
for (int v = 0; v < G.Vertices; v++)
_distTo[v] = INFINITY;
_distTo[s] = 0;
_marked[s] = true;
q.Enqueue(s);
while (q.Any())
{
int v = q.Dequeue();
foreach (int w in G.Adj[v])
{
if (!_marked[w])
{
_edgeTo[w] = v;
_distTo[w] = _distTo[v] + 1;
_marked[w] = true;
q.Enqueue(w);
}
}
}
}
private void bfs(Graph G, IEnumerable<int> sources)
{
Queue<int> q = new Queue<int>();
foreach (int s in sources)
{
_marked[s] = true;
_distTo[s] = 0;
q.Enqueue(s);
}
while (q.Any())
{
int v = q.Dequeue();
foreach (int w in G.Adj[v])
{
if (!_marked[w])
{
_edgeTo[w] = v;
_distTo[w] = _distTo[v] + 1;
_marked[w] = true;
q.Enqueue(w);
}
}
}
}
public bool HasPathTo(int v)
{
validateVertex(v);
return _marked[v];
}
public int DistTo(int v)
{
validateVertex(v);
return _distTo[v];
}
public IEnumerable<int> PathTo(int v)
{
validateVertex(v);
if (!HasPathTo(v)) return null;
Stack<int> path = new Stack<int>();
int x;
for (x = v; _distTo[x] != 0; x = _edgeTo[x])
path.Push(x);
path.Push(x);
return path;
}
private void validateVertex(int v)
{
int V = _marked.Length;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
private void validateVertices(IEnumerable<int> vertices)
{
if (vertices == null)
{
throw new ArgumentException("argument is null");
}
int V = _marked.Length;
foreach (int v in vertices)
{
if (v < 0 || v >= V)
{
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
}
}
測試:
[TestMethod()]
public void BreadthFirstPathsTest()
{
var data = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data\\tinyG.txt");
Console.WriteLine(data);
using (StreamReader stream = new StreamReader(data))
{
AlgorithmEngine.Graph.Graph G = new AlgorithmEngine.Graph.Graph(stream);
int s =1;
BreadthFirstPaths bfs = new BreadthFirstPaths(G, s);
for (int v = 0; v < G.Vertices; v++)
{
if (bfs.HasPathTo(v))
{
Console.Write($"{s} to {v} ({bfs.DistTo(v)}): ");
foreach (int x in bfs.PathTo(v))
{
if (x == s) Console.Write(x);
else Console.Write("-" + x);
}
Console.Write(System.Environment.NewLine);
}
else
{
Console.Write($"{s} to{v} (-): not connected\n");
}
}
}
//1 to 0(1): 1 - 0
//1 to 1(0): 1
//1 to 2(2): 1 - 0 - 2
//1 to 3(3): 1 - 0 - 5 - 3
//1 to 4(3): 1 - 0 - 6 - 4
//1 to 5(2): 1 - 0 - 5
//1 to 6(2): 1 - 0 - 6
//1 to7(-): not connected
//1 to8(-): not connected
//1 to9(-): not connected
//1 to10(-): not connected
//1 to11(-): not connected
//1 to12(-): not connected
}
命題B:對於從S可達的任意頂點V,edgeTo[]陣列在第二步之後就已經完成了。和深度優先搜尋一樣,一旦所有的頂點都已經被標記,餘下的計算工作就只是在檢查連線到各個已被標記的頂點的邊而已。
命題B:廣度搜索所需的時間在最壞情況下和V+E成正比。
二分圖
/// <summary>
/// 使用廣度優先搜尋
/// </summary>
public class BipartiteX
{
private static readonly bool WHITE = false;
private static readonly bool BLACK = true;
private bool _isBipartite;
private bool[] _color;
private bool[] _marked;
private int[] _edgeTo;
private Queue<int> _cycle;
public bool IsBipartite => _isBipartite;
public BipartiteX(Graph G)
{
_isBipartite = true;
_color = new bool[G.Vertices];
_marked = new bool[G.Vertices];
_edgeTo = new int[G.Vertices];
for (int v = 0; v < G.Vertices && _isBipartite; v++)
{
if (!_marked[v])
{
bfs(G, v);
}
}
}
private void bfs(Graph G, int s)
{
Queue<int> q = new Queue<int>();
_color[s] = WHITE;
_marked[s] = true;
q.Enqueue(s);
while (q.Any())
{
int v = q.Dequeue();
foreach (int w in G.Adj[v])
{
if (!_marked[w])
{
_marked[w] = true;
_edgeTo[w] = v;
_color[w] = !_color[v];
q.Enqueue(w);
}
else if (_color[w] == _color[v])
{
_isBipartite = false;//不是二分圖
//奇迴圈:整個路徑的構成 (w-x)+(x-v)+(v-w)
_cycle = new Queue<int>();
Stack<int> stack = new Stack<int>();
int x = v, y = w;
while (x != y)
{
stack.Push(x);
_cycle.Enqueue(y);
x = _edgeTo[x];
y = _edgeTo[y];
}
stack.Push(x);
while (stack.Any())//將上面的頂點和下面的頂點合併
_cycle.Enqueue(stack.Pop());
_cycle.Enqueue(w);
return;
}
}
}
}
public bool Color(int v)
{
validateVertex(v);
if (!_isBipartite)
throw new ArgumentException("Graph is not bipartite");
return _color[v];
}
public IEnumerable<int> OddCycle()
{
return _cycle;
}
private bool check(Graph G)
{
// graph is bipartite
if (_isBipartite)
{
for (int v = 0; v < G.Vertices; v++)
{
foreach (int w in G.Adj[v])
{
if (_color[v] == _color[w])
{
Console.Write($"edge {v}-{w} with {v} and {w} in same side of bipartition\n");
return false;
}
}
}
}
// graph has an odd-length cycle
else
{
// verify cycle
int first = -1, last = -1;
foreach (int v in OddCycle())
{
if (first == -1) first = v;
last = v;
}
if (first != last)
{
Console.Write($"cycle begins with {first} and ends with {last}\n", first, last);
return false;
}
}
return true;
}
// throw an IllegalArgumentException unless {@code 0 <= v < V}
private void validateVertex(int v)
{
int V = _marked.Length;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
測試:
[TestMethod()]
public void BipartiteXTest()
{
AlgorithmEngine.Graph.Graph G = GraphGenerator.Bipartite(6, 6, 20);
Random random = new Random();
//for (int i = 0; i < 1; i++)
//{
// int v = random.Next(12);
// int w = random.Next(12);
// G.AddEdge(v, w);
//}
BipartiteX b = new BipartiteX(G);
if (b.IsBipartite)
{
Console.Write("Graph is bipartite: ");
for (int v = 0; v < G.Vertices; v++)
{
Console.Write(v + ": " + b.Color(v) + "; ");
}
}
else
{
Console.Write("Graph has an odd-length cycle: ");
foreach (int x in b.OddCycle())
{
Console.Write(x + " ");
}
Console.WriteLine();
}
}
深度搜索和廣度搜索
深度優先:不斷深入圖,棧中儲存了所有分叉的頂點
廣度優先:像扇面掃描一樣,佇列中儲存了所有訪問過的最前端節點。
連通分量
深度搜索比Union-Find快,但實際Union-Find更快,因為Union-Find不需要完整構建整個圖,
當只需要判斷連通性,需要有大量連通性查詢和插入混合操作時,推薦使用Union-Find演算法
當需要圖的抽象資料型別的時候,推薦使用深度優先。
符號圖
在實際使用中,圖都是通過檔案和網頁定義了,使用的是字串來代替頂點。
- 頂點為字串
- 用指定分隔符來隔開頂點名
- 每一行都表示一組邊的集合。
- 頂點總數V和邊的總數都是隱式定義的。
public class SymbolGraph
{
private ST<String, int> _st; //符號名-索引
private String[] _keys; // 索引-符號名
private Graph _graph; // 圖
public SymbolGraph(String filename, String delimiter)
{
_st = new ST<String, int>();
var stream = new StreamReader(filename);
while (!stream.EndOfStream) {//第一遍構造頂點
String[] a = stream.ReadLine().Split(delimiter.ToCharArray());
for (int i = 0; i < a.Length; i++)
{
if (!_st.Contains(a[i]))//為每個不同的字串關聯一個索引
_st.Add(a[i], _st.Count());
}
}
_keys = new String[_st.Count()];//用來獲得頂點名的反向索引是一個數組
foreach (String name in _st.Keys())
{
_keys[_st.Get(name)] = name;
}
_graph = new Graph(_st.Count());
stream = new StreamReader(filename);//第二遍構造邊
while (!stream.EndOfStream) {
String[] a = stream.ReadLine().Split(delimiter.ToCharArray());//將每一行的頂點和該行的其他頂點相連
int v = _st.Get(a[0]);
for (int i = 1; i < a.Length; i++)
{
int w = _st.Get(a[i]);
_graph.AddEdge(v, w);
}
}
}
public bool Contains(String s)
{
return _st.Contains(s);
}
public int Index(String s)
{
return _st.Get(s);
}
public int IndexOf(String s)
{
return _st.Get(s);
}
public String Name(int v)
{
validateVertex(v);
return _keys[v];
}
public String NameOf(int v)
{
validateVertex(v);
return _keys[v];
}
public Graph Graph()
{
return _graph;
}
private void validateVertex(int v)
{
int V = _graph.Vertices;
if (v < 0 || v >= V)
throw new ArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
}
}
測試:
[TestMethod()]
public void SymbolGraph()
{
var data = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data\\routes.txt");
SymbolGraph sg = new SymbolGraph(data, " ");
AlgorithmEngine.Graph.Graph graph = sg.Graph();
StreamReader reader=new StreamReader(data);
while (!reader.EndOfStream)
{
String source = reader.ReadLine().Split(' ')[0];
if (sg.Contains(source))
{
Console.Write($"{source} :");
int s = sg.Index(source);
foreach (int v in graph.Adj[s])
{
Console.Write(" " + sg.Name(v));
}
}
else
{
Console.WriteLine("input not contain '" + source + "'");
}
Console.WriteLine();
}
// JFK: ORD ATL MCO
// ORD : ATL JFK PHX DFW HOU DEN
// ORD: ATL JFK PHX DFW HOU DEN
// DFW: HOU ORD PHX
// JFK : ORD ATL MCO
// ORD : ATL JFK PHX DFW HOU DEN
// ORD: ATL JFK PHX DFW HOU DEN
// ATL: MCO ORD HOU JFK
// DEN: LAS PHX ORD
// PHX : LAS LAX DEN ORD DFW
// JFK : ORD ATL MCO
// DEN : LAS PHX ORD
// DFW : HOU ORD PHX
// ORD : ATL JFK PHX DFW HOU DEN
// LAS: PHX LAX DEN
// ATL : MCO ORD HOU JFK
// HOU: MCO DFW ATL ORD
// LAS: PHX LAX DEN
}