寫在前面
整個項目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp
這一節內容可能會用到的庫文件有 Measurement 和 TestCase,同樣在 Github 上可以找到。
善用 Ctrl + F 查找題目。
習題&題解
1.5.1
解答
quick-find 的官方實現:QuickFindUF.java。
只要實現相應並查集,然后輸入內容即可。
增加一個記錄訪問數組次數的類成員變量,在每次訪問數組的語句執行后自增即可。
樣例輸出:
0 1 2 3 4 5 6 7 8 0 數組訪問:13 0 1 2 4 4 5 6 7 8 0 數組訪問:13 0 1 2 4 4 8 6 7 8 0 數組訪問:13 0 1 2 4 4 8 6 2 8 0 數組訪問:13 0 1 1 4 4 8 6 1 8 0 數組訪問:14 0 1 1 4 4 1 6 1 1 0 數組訪問:14 4 1 1 4 4 1 6 1 1 4 數組訪問:14 1 1 1 1 1 1 6 1 1 1 數組訪問:16
代碼
QuickFindUF.cs,這個類繼承了 UF.cs,重新實現了 Union() 和 Find() 等方法。
關於 UF.cs 可以參見原書中文版 P138 或英文版 P221 的算法 1.5。
namespace UnionFind { /// <summary> /// 用 QuickFind 算法實現的並查集。 /// </summary> public class QuickFindUF : UF { public int ArrayVisitCount { get; private set; } //記錄數組訪問的次數。 /// <summary> /// 新建一個使用 quick-find 實現的並查集。 /// </summary> /// <param name="n">並查集的大小。</param> public QuickFindUF(int n) : base(n) { } /// <summary> /// 重置數組訪問計數。 /// </summary> public void ResetArrayCount() { this.ArrayVisitCount = 0; } /// <summary> /// 尋找 p 所在的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>返回 p 所在的連通分量。</returns> public override int Find(int p) { Validate(p); this.ArrayVisitCount++; return this.parent[p]; } /// <summary> /// 判斷兩個結點是否屬於同一個連通分量。 /// </summary> /// <param name="p">需要判斷的結點。</param> /// <param name="q">需要判斷的另一個結點。</param> /// <returns>如果屬於同一個連通分量則返回 true,否則返回 false。</returns> public override bool IsConnected(int p, int q) { Validate(p); Validate(q); this.ArrayVisitCount += 2; return this.parent[p] == this.parent[q]; } /// <summary> /// 將兩個結點所在的連通分量合並。 /// </summary> /// <param name="p">需要合並的結點。</param> /// <param name="q">需要合並的另一個結點。</param> public override void Union(int p, int q) { Validate(p); Validate(q); int pID = this.parent[p]; int qID = this.parent[q]; this.ArrayVisitCount += 2; // 如果兩個結點同屬於一個連通分量,那么什么也不做。 if (pID == qID) { return; } for (int i = 0; i < this.parent.Length; ++i) { if (this.parent[i] == pID) { this.parent[i] = qID; this.ArrayVisitCount++; } } this.ArrayVisitCount += this.parent.Length; this.count--; return; } /// <summary> /// 獲得 parent 數組。 /// </summary> /// <returns>id 數組。</returns> public int[] GetParent() { return this.parent; } } }
Main 方法:
using System; using UnionFind; namespace _1._5._1 { /* * 1.5.1 * * 使用 quick-find 算法處理序列 9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2 。 * 對於輸入的每一對整數,給出 id[] 數組的內容和訪問數組的次數。 * */ class Program { static void Main(string[] args) { string[] input = "9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2".Split(' '); var quickFind = new QuickFindUF(10); foreach (string s in input) { quickFind.ResetArrayCount(); string[] numbers = s.Split('-'); int p = int.Parse(numbers[0]); int q = int.Parse(numbers[1]); int[] id = quickFind.GetParent(); quickFind.Union(p, q); foreach (int root in id) { Console.Write(root + " "); } Console.WriteLine("數組訪問:" + quickFind.ArrayVisitCount); } } } }
1.5.2
解答
quick-union 的官方實現:QuickUnionUF.java。
和上題一樣的方式,增加一個記錄訪問數組次數的類成員變量,在每次訪問數組的語句執行后自增即可。
程序輸出的森林,用縮進表示子樹:
|---- 0 |---- 9 |---- 1 |---- 2 |---- 3 |---- 4 |---- 5 |---- 6 |---- 7 |---- 8 數組訪問:1 |---- 0 |---- 9 |---- 1 |---- 2 |---- 4 |---- 3 |---- 5 |---- 6 |---- 7 |---- 8 數組訪問:1 |---- 0 |---- 9 |---- 1 |---- 2 |---- 4 |---- 3 |---- 6 |---- 7 |---- 8 |---- 5 數組訪問:1 |---- 0 |---- 9 |---- 1 |---- 2 |---- 7 |---- 4 |---- 3 |---- 6 |---- 8 |---- 5 數組訪問:1 |---- 0 |---- 9 |---- 1 |---- 2 |---- 7 |---- 4 |---- 3 |---- 6 |---- 8 |---- 5 數組訪問:1 |---- 0 |---- 9 |---- 1 |---- 2 |---- 7 |---- 8 |---- 5 |---- 4 |---- 3 |---- 6 數組訪問:7 |---- 1 |---- 2 |---- 7 |---- 8 |---- 5 |---- 4 |---- 0 |---- 9 |---- 3 |---- 6 數組訪問:3 |---- 1 |---- 2 |---- 7 |---- 4 |---- 0 |---- 9 |---- 3 |---- 8 |---- 5 |---- 6 數組訪問:3
代碼
QuickUnionUF.cs,這個類繼承了 UF.cs,重新實現了 Union() 和 Find() 等方法。
關於 UF.cs 可以參見原書中文版 P138 或英文版 P221 的算法 1.5。
namespace UnionFind { /// <summary> /// 用 QuickUnion 算法實現的並查集。 /// </summary> public class QuickUnionUF : UF { public int ArrayVisitCount { get; private set; } //記錄數組訪問的次數。 /// <summary> /// 建立使用 QuickUnion 的並查集。 /// </summary> /// <param name="n">並查集的大小。</param> public QuickUnionUF(int n) : base(n) { } /// <summary> /// 重置數組訪問計數。 /// </summary> public virtual void ResetArrayCount() { this.ArrayVisitCount = 0; } /// <summary> /// 獲得 parent 數組。 /// </summary> /// <returns>返回 parent 數組。</returns> public int[] GetParent() { return this.parent; } /// <summary> /// 尋找一個結點所在的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>該結點所屬的連通分量。</returns> public override int Find(int p) { Validate(p); while (p != this.parent[p]) { p = this.parent[p]; this.ArrayVisitCount += 2; } return p; } /// <summary> /// 將兩個結點所屬的連通分量合並。 /// </summary> /// <param name="p">需要合並的結點。</param> /// <param name="q">需要合並的另一個結點。</param> public override void Union(int p, int q) { int rootP = Find(p); int rootQ = Find(q); if (rootP == rootQ) { return; } this.parent[rootP] = rootQ; this.ArrayVisitCount++; this.count--; } } }
Main 方法
using System; using UnionFind; namespace _1._5._2 { /* * 1.5.2 * * 使用 quick-union 算法(請見 1.5.2.3 節代碼框)完成練習 1.5.1。 * 另外,在處理完輸入的每對整數之后畫出 id[] 數組表示的森林。 * */ class Program { static void Main(string[] args) { string[] input = "9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2".Split(' '); var quickUnion = new QuickUnionUF(10); foreach (string s in input) { quickUnion.ResetArrayCount(); string[] numbers = s.Split('-'); int p = int.Parse(numbers[0]); int q = int.Parse(numbers[1]); quickUnion.Union(p, q); int[] parent = quickUnion.GetParent(); for (int i = 0; i < parent.Length; ++i) { if (parent[i] == i) { Console.WriteLine("|---- " + i); DFS(parent, i, 1); } } Console.WriteLine("數組訪問:" + quickUnion.ArrayVisitCount); } } static void DFS(int[] parent, int root, int level) { for (int i = 0; i < parent.Length; ++i) { if (parent[i] == root && i != root) { for (int j = 0; j < level; ++j) { Console.Write(" "); } Console.WriteLine("|---- " + i); DFS(parent, i, level + 1); } } } } }
1.5.3
解答
加權 quick-union 的官方實現:WeightedQuickUnionUF.java。
樣例輸出:
9 1 2 3 4 5 6 7 8 9 數組訪問:3 9 1 2 3 3 5 6 7 8 9 數組訪問:3 9 1 2 3 3 5 6 7 5 9 數組訪問:3 9 1 7 3 3 5 6 7 5 9 數組訪問:3 9 7 7 3 3 5 6 7 5 9 數組訪問:5 9 7 7 3 3 7 6 7 5 9 數組訪問:3 9 7 7 9 3 7 6 7 5 9 數組訪問:5 9 7 7 9 3 7 6 7 5 7 數組訪問:9
代碼
WeightedQuickUnionUF.cs,這個類繼承了 QuickUnion.cs,重新實現了 Union() 和 Find() 等方法。
關於 QuickUnion.cs 可以參見 1.5.2 的代碼部分。
namespace UnionFind { /// <summary> /// 使用加權 quick-union 算法的並查集。 /// </summary> public class WeightedQuickUnionUF : QuickUnionUF { protected int[] size; // 記錄各個樹的大小。 public int ArrayParentVisitCount { get; private set; } // 記錄 parent 數組的訪問次數。 public int ArraySizeVisitCount { get; private set; } //記錄 size 數組的訪問次數。 /// <summary> /// 建立使用加權 quick-union 的並查集。 /// </summary> /// <param name="n">並查集的大小。</param> public WeightedQuickUnionUF(int n) : base(n) { this.size = new int[n]; for (int i = 0; i < n; ++i) { this.size[i] = 1; } this.ArrayParentVisitCount = 0; this.ArraySizeVisitCount = 0; } /// <summary> /// 清零數組訪問計數。 /// </summary> public override void ResetArrayCount() { this.ArrayParentVisitCount = 0; this.ArraySizeVisitCount = 0; } /// <summary> /// 獲取 size 數組。 /// </summary> /// <returns>返回 size 數組。</returns> public int[] GetSize() { return this.size; } /// <summary> /// 尋找一個結點所在的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>該結點所屬的連通分量。</returns> public override int Find(int p) { Validate(p); while (p != this.parent[p]) { p = this.parent[p]; this.ArrayParentVisitCount += 2; } this.ArrayParentVisitCount++; return p; } /// <summary> /// 將兩個結點所屬的連通分量合並。 /// </summary> /// <param name="p">需要合並的結點。</param> /// <param name="q">需要合並的另一個結點。</param> public override void Union(int p, int q) { int rootP = Find(p); int rootQ = Find(q); if (rootP == rootQ) { return; } if (this.size[rootP] < this.size[rootQ]) { this.parent[rootP] = rootQ; this.size[rootQ] += this.size[rootP]; } else { this.parent[rootQ] = rootP; this.size[rootP] += this.size[rootQ]; } this.ArrayParentVisitCount++; this.ArraySizeVisitCount += 4; this.count--; } } }
Main 方法
using System; using UnionFind; namespace _1._5._3 { /* * 1.5.3 * * 使用加權 quick-union 算法(請見算法 1.5)完成練習 1.5.1 。 * */ class Program { static void Main(string[] args) { string[] input = "9-0 3-4 5-8 7-2 2-1 5-7 0-3 4-2".Split(' '); var weightedQuickUnion = new WeightedQuickUnionUF(10); foreach (string s in input) { weightedQuickUnion.ResetArrayCount(); string[] numbers = s.Split('-'); int p = int.Parse(numbers[0]); int q = int.Parse(numbers[1]); weightedQuickUnion.Union(p, q); int[] parent = weightedQuickUnion.GetParent(); for (int i = 0; i < parent.Length; ++i) { Console.Write(parent[i] + " "); } Console.WriteLine("數組訪問:" + weightedQuickUnion.ArrayParentVisitCount); } } } }
1.5.4
解答
對照輸入和最壞輸入均在書中出現,中文版見:P146,英文版見:P229。
樣例輸出:
4 3 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 1 2 4 4 5 6 7 8 9 size: 1 1 1 1 2 1 1 1 1 1 parent visit count:3 size visit count:4 3 8 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 1 2 4 4 5 6 7 4 9 size: 1 1 1 1 3 1 1 1 1 1 parent visit count:5 size visit count:4 6 5 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 1 2 4 4 6 6 7 4 9 size: 1 1 1 1 3 1 2 1 1 1 parent visit count:3 size visit count:4 9 4 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 1 2 4 4 6 6 7 4 4 size: 1 1 1 1 4 1 2 1 1 1 parent visit count:3 size visit count:4 2 1 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 2 2 4 4 6 6 7 4 4 size: 1 1 2 1 4 1 2 1 1 1 parent visit count:3 size visit count:4 8 9 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 2 2 4 4 6 6 7 4 4 size: 1 1 2 1 4 1 2 1 1 1 parent visit count:6 size visit count:0 5 0 index: 0 1 2 3 4 5 6 7 8 9 parent: 6 2 2 4 4 6 6 7 4 4 size: 1 1 2 1 4 1 3 1 1 1 parent visit count:5 size visit count:4 7 2 index: 0 1 2 3 4 5 6 7 8 9 parent: 6 2 2 4 4 6 6 2 4 4 size: 1 1 3 1 4 1 3 1 1 1 parent visit count:3 size visit count:4 6 1 index: 0 1 2 3 4 5 6 7 8 9 parent: 6 2 6 4 4 6 6 2 4 4 size: 1 1 3 1 4 1 6 1 1 1 parent visit count:5 size visit count:4 1 0 index: 0 1 2 3 4 5 6 7 8 9 parent: 6 2 6 4 4 6 6 2 4 4 size: 1 1 3 1 4 1 6 1 1 1 parent visit count:8 size visit count:0 6 7 index: 0 1 2 3 4 5 6 7 8 9 parent: 6 2 6 4 4 6 6 2 4 4 size: 1 1 3 1 4 1 6 1 1 1 parent visit count:6 size visit count:0 ------------------------------------- 0 1 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 2 3 4 5 6 7 8 9 size: 2 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 2 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 3 4 5 6 7 8 9 size: 3 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 3 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 0 4 5 6 7 8 9 size: 4 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 4 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 0 0 5 6 7 8 9 size: 5 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 5 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 0 0 0 6 7 8 9 size: 6 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 6 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 0 0 0 0 7 8 9 size: 7 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 7 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 0 0 0 0 0 8 9 size: 8 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 8 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 0 0 0 0 0 0 9 size: 9 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4 0 9 index: 0 1 2 3 4 5 6 7 8 9 parent: 0 0 0 0 0 0 0 0 0 0 size: 10 1 1 1 1 1 1 1 1 1 parent visit count:3 size visit count:4
代碼
Main 方法:
using System; using UnionFind; namespace _1._5._4 { /* * 1.5.4 * * 在正文的加權 quick-union 算法示例中, * 對於輸入的每一對整數(包括對照輸入和最壞情況下的輸入), * 給出 id[] 和 sz[] 數組的內容以及訪問數組的次數。 * */ class Program { static void Main(string[] args) { char[] split = { '\n', '\r' }; string[] inputReference = TestCase.Properties.Resources.tinyUF.Split(split, StringSplitOptions.RemoveEmptyEntries); string[] inputWorst = TestCase.Properties.Resources.worstUF.Split(split, StringSplitOptions.RemoveEmptyEntries); RunTest(inputReference); Console.WriteLine("-------------------------------------"); RunTest(inputWorst); } static void RunTest(string[] input) { var weightedQuickUnion = new WeightedQuickUnionUF(10); int n = int.Parse(input[0]); int[] parent = weightedQuickUnion.GetParent(); int[] size = weightedQuickUnion.GetSize(); for (int i = 1; i < input.Length; ++i) { string[] unit = input[i].Split(' '); int p = int.Parse(unit[0]); int q = int.Parse(unit[1]); Console.WriteLine($"{p} {q}"); weightedQuickUnion.Union(p, q); Console.Write("index:\t"); for (int j = 0; j < 10; ++j) { Console.Write(j + " "); } Console.WriteLine(); Console.Write("parent:\t"); foreach (int m in parent) { Console.Write(m + " "); } Console.WriteLine(); Console.Write("size:\t"); foreach (int m in size) { Console.Write(m + " "); } Console.WriteLine(); Console.WriteLine("parent visit count:" + weightedQuickUnion.ArrayParentVisitCount); Console.WriteLine("size visit count:" + weightedQuickUnion.ArraySizeVisitCount); Console.WriteLine(); weightedQuickUnion.ResetArrayCount(); } } } }
1.5.5
解答
106 條連接 = 106 組輸入。
對於 quick-find 算法,每次 union() 都要遍歷整個數組。
因此總共進行了 109 * 106 = 1015 次 for 循環迭代。
每次 for 循環迭代都需要 10 條機器指令,
因此總共執行了 10 * 1015 = 1016 條機器指令。
已知計算機每秒能夠執行 109 條機器指令,
因此執行完所有指令需要 1016 / 109 = 107 秒 = 115.74 天
1.5.6
解答
加權 quick-union 算法最多只需要 lgN 次迭代就可以完成一次 union()。
因此按照上題思路,總共需要 (lg(109) * 106 * 10) / 109 = 0.299 秒。
1.5.7
解答
見 1.5.1 和 1.5.2 的解答。
1.5.8
解答
當有多個元素需要修改的時候,這個直觀算法可能會出現錯誤。
例如如下情況:
index 0 1 2 3 4 5 6 7 8 9
id 0 0 0 0 0 5 5 5 5 5
輸入 0, 5
i = 0 時,id[i] == id[p],此時 id[i] = id[q]。
數組變為 5 0 0 0 0 5 5 5 5 5
i = 1 時,id[i] != id[p],算法出現錯誤。
如果在 id[p] 之后還有需要修改的元素,那么這個算法就會出現錯誤。
1.5.9
解答
由於加權 quick-union 算法任意節點的最大深度為 lgN (節點總數為 N)。
(這個結論可以在中文版 P146,或者英文版 P228 找到)
上面這個樹的最大深度為 4 > lg10
因此這棵樹不可能是通過加權 quick-union 算法得到的。
1.5.10
解答
本題答案已經給出,也很好理解。
如果合並時只是把子樹掛到結點 q 上而非其根節點,樹的高度會明顯增加,進而增加每次 Find() 操作的開銷。
1.5.11
解答
類似於加權 quick-union 的做法,新增一個 size[] 數組以記錄各個根節點的大小。
每次合並時先比較一下兩棵樹的大小,再進行合並。
這樣會略微減少賦值語句的執行次數,提升性能。
代碼
WeightedQuickFindUF.cs
using System; namespace _1._5._11 { /// <summary> /// 用加權 QuickFind 算法實現的並查集。 /// </summary> public class WeightedQuickFindUF { private int[] size; // 記錄每個連通分量的大小。 private int[] id; // 記錄每個結點的連通分量。 private int count;// 連通分量總數。 public int ArrayVisitCount { get; private set; } //記錄數組訪問的次數。 /// <summary> /// 新建一個使用加權 quick-find 實現的並查集。 /// </summary> /// <param name="n">並查集的大小。</param> public WeightedQuickFindUF(int n) { this.count = n; this.id = new int[n]; this.size = new int[n]; for (int i = 0; i < n; ++i) { this.id[i] = i; this.size[i] = 1; } } /// <summary> /// 重置數組訪問計數。 /// </summary> public void ResetArrayCount() { this.ArrayVisitCount = 0; } /// <summary> /// 表示並查集中連通分量的數量。 /// </summary> /// <returns>返回並查集中連通分量的數量。</returns> public int Count() { return this.count; } /// <summary> /// 尋找 p 所在的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>返回 p 所在的連通分量。</returns> public int Find(int p) { Validate(p); this.ArrayVisitCount++; return this.id[p]; } /// <summary> /// 判斷兩個結點是否屬於同一個連通分量。 /// </summary> /// <param name="p">需要判斷的結點。</param> /// <param name="q">需要判斷的另一個結點。</param> /// <returns>如果屬於同一個連通分量則返回 true,否則返回 false。</returns> public bool IsConnected(int p, int q) { Validate(p); Validate(q); this.ArrayVisitCount += 2; return this.id[p] == this.id[q]; } /// <summary> /// 將兩個結點所在的連通分量合並。 /// </summary> /// <param name="p">需要合並的結點。</param> /// <param name="q">需要合並的另一個結點。</param> public void Union(int p, int q) { Validate(p); Validate(q); int pID = this.id[p]; int qID = this.id[q]; this.ArrayVisitCount += 2; // 如果兩個結點同屬於一個連通分量,那么什么也不做。 if (pID == qID) { return; } // 判斷較大的連通分量和較小的連通分量。 int larger = 0; int smaller = 0; if (this.size[pID] > this.size[qID]) { larger = pID; smaller = qID; this.size[pID] += this.size[qID]; } else { larger = qID; smaller = pID; this.size[qID] += this.size[pID]; } // 將較小的連通分量連接到較大的連通分量上, // 這會減少賦值語句的執行次數,略微減少數組訪問。 for (int i = 0; i < this.id.Length; ++i) { if (this.id[i] == smaller) { this.id[i] = larger; this.ArrayVisitCount++; } } this.ArrayVisitCount += this.id.Length; this.count--; return; } /// <summary> /// 獲得 id 數組。 /// </summary> /// <returns>id 數組。</returns> public int[] GetID() { return this.id; } /// <summary> /// 驗證輸入的結點是否有效。 /// </summary> /// <param name="p">需要驗證的結點。</param> /// <exception cref="ArgumentException">輸入的 p 值無效。</exception> private void Validate(int p) { int n = this.id.Length; if (p < 0 || p > n) { throw new ArgumentException("index " + p + " is not between 0 and " + (n - 1)); } } } }
Main 方法
using System; using UnionFind; namespace _1._5._11 { /* * 1.5.11 * * 實現加權 quick-find 算法,其中我們總是將較小的分量重命名為較大分量的標識符。 * 這種改變會對性能產生怎樣的影響? * */ class Program { static void Main(string[] args) { char[] split = { '\n', '\r' }; string[] input = TestCase.Properties.Resources.mediumUF.Split(split, StringSplitOptions.RemoveEmptyEntries); int size = int.Parse(input[0]); QuickFindUF quickFind = new QuickFindUF(size); WeightedQuickFindUF weightedQuickFind = new WeightedQuickFindUF(size); int p, q; string[] pair; for (int i = 1; i < size; ++i) { pair = input[i].Split(' '); p = int.Parse(pair[0]); q = int.Parse(pair[1]); quickFind.Union(p, q); weightedQuickFind.Union(p, q); } Console.WriteLine("quick-find: " + quickFind.ArrayVisitCount); Console.WriteLine("weighted quick-find: " + weightedQuickFind.ArrayVisitCount); } } }
1.5.12
解答
QuickUnionPathCompression 的官方實現:QuickUnionPathCompressionUF.java
在找到根節點之后,再訪問一遍 p 到根節點這條路徑上的所有結點,將它們直接和根節點相連。
重寫過后的 Find() 方法:
/// <summary> /// 尋找結點所屬的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>結點所屬的連通分量。</returns> public override int Find(int p) { int root = p; while (root != this.parent[root]) { root = this.parent[root]; } while (p != root) { int newp = this.parent[p]; this.parent[p] = root; p = newp; } return p; }
由於路徑壓縮是在 Find() 方法中實現的,只要輸入保證是根節點兩兩相連即可構造較長的路徑。
代碼
QuickUnionPathCompressionUF.cs 直接從 QuickUnionUF.cs 繼承而來。
關於 QuickUnionUF.cs,參見 1.5.2 的解答。
namespace UnionFind { /// <summary> /// 使用路徑壓縮的 quick-union 並查集。 /// </summary> public class QuickUnionPathCompressionUF : QuickFindUF { /// <summary> /// 新建一個大小為 n 的並查集。 /// </summary> /// <param name="n">新建並查集的大小。</param> public QuickUnionPathCompressionUF(int n) : base(n) { } /// <summary> /// 尋找結點所屬的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>結點所屬的連通分量。</returns> public override int Find(int p) { int root = p; while (root != this.parent[root]) { root = this.parent[root]; } while (p != root) { int newp = this.parent[p]; this.parent[p] = root; p = newp; } return p; } } }
Main 方法
using System; using UnionFind; namespace _1._5._12 { /* * 1.5.12 * * 使用路徑壓縮的 quick-union 算法。 * 根據路徑壓縮修改 quick-union 算法(請見 1.5.2.3 節), * 在 find() 方法中添加一個循環來將從 p 到根節點的路徑上的每個觸點都連接到根節點。 * 給出一列輸入,使該方法能夠產生一條長度為 4 的路徑。 * 注意:該算法的所有操作的均攤成本已知為對數級別。 * */ class Program { static void Main(string[] args) { var UF = new QuickUnionPathCompressionUF(10); // 使用書中提到的最壞情況,0 連 1,1 連 2,2 連 3…… for (int i = 0; i < 4; ++i) { UF.Union(i, i + 1); } int[] id = UF.GetParent(); for (int i = 0; i < id.Length; ++i) { Console.Write(id[i]); } Console.WriteLine(); } } }
1.5.13
解答
官方實現:WeightedQuickUnionPathCompressionUF。
加權 quick-union 中,兩個大小相等的樹合並可以有效增加高度,同時輸入必須保證是根節點以規避路徑壓縮。
代碼
WeightedQuickUnionPathCompressionUF.cs 從 WeightedQuickUnionUF.cs 繼承,詳情參見 1.5.3 的解答。
namespace UnionFind { /// <summary> /// 使用路徑壓縮的加權 quick-union 並查集。 /// </summary> public class WeightedQuickUnionPathCompressionUF : WeightedQuickUnionUF { /// <summary> /// 新建一個大小為 n 的並查集。 /// </summary> /// <param name="n">新建並查集的大小。</param> public WeightedQuickUnionPathCompressionUF(int n) : base(n) { this.size = new int[n]; for (int i = 0; i < n; ++i) { this.size[i] = 1; } } /// <summary> /// 尋找一個結點所在的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>該結點所屬的連通分量。</returns> public override int Find(int p) { Validate(p); int root = p; while (root != this.parent[p]) { root = this.parent[p]; } while (p != root) { int newP = this.parent[p]; this.parent[p] = root; p = newP; } return root; } } }
Main 方法
using System; using UnionFind; namespace _1._5._13 { /* * 1.5.13 * * 使用路徑壓縮的加權 quick-union 算法。 * 修改加權 quick-union 算法(算法 1.5), * 實現如練習 1.5.12 所述的路徑壓縮。給出一列輸入, * 使該方法能產生一棵高度為 4 的樹。 * 注意:該算法的所有操作的均攤成本已知被限制在反 Ackermann 函數的范圍之內, * 且對於實際應用中可能出現的所有 N 值均小於 5。 * */ class Program { static void Main(string[] args) { var UF = new WeightedQuickUnionPathCompressionUF(10); // 見中文版 P146 或英文版 P229 中加權 quick-union 的最壞輸入。 UF.Union(0, 1); UF.Union(2, 3); UF.Union(4, 5); UF.Union(6, 7); UF.Union(0, 2); UF.Union(4, 6); UF.Union(0, 4); int[] id = UF.GetParent(); for (int i = 0; i < id.Length; ++i) { Console.Write(id[i]); } Console.WriteLine(); } } }
1.5.14
解答
WeightedQuickUnionByHeight 的官方實現:WeightedQuickUnionByHeightUF.java。
證明:
一次 Union 操作只可能發生如下兩種情況。
1.兩棵樹的高度相同,這樣合並后的新樹的高度等於較大那棵樹的高度 + 1。
2.兩棵樹的高度不同,這樣合並后的新樹高度等於較大那棵樹的高度。
現在證明通過加權 quick-union 算法構造的高度為 h 的樹至少包含 2h 個結點。
基礎情況,高度 h = 0, 結點數 k = 1。
為了使高度增加,必須用一棵高度相同的樹合並,而 h = 0 時結點數一定是 1,則:
h = 1, k = 2
由於兩棵大小不同的樹合並,最大高度不會增加,只會增加結點數。
因此,每次都使用相同高度的最小樹進行合並,有:
h = 2, k = 4
h = 3, k = 8
h = 4, k = 16
......
遞推即可得到結論,k ≥ 2h
因此 h <= lgk
代碼
namespace UnionFind { public class WeightedQuickUnionByHeightUF : QuickUnionUF { private int[] height; /// <summary> /// 新建一個以高度作為判斷依據的加權 quick-union 並查集。 /// </summary> /// <param name="n">新建並查集的大小。</param> public WeightedQuickUnionByHeightUF(int n) : base(n) { this.height = new int[n]; for (int i = 0; i < n; ++i) { this.height[i] = 0; } } /// <summary> /// 將兩個結點所屬的連通分量合並。 /// </summary> /// <param name="p">需要合並的結點。</param> /// <param name="q">需要合並的另一個結點。</param> public override void Union(int p, int q) { int rootP = Find(p); int rootQ = Find(q); if (rootP == rootQ) { return; } if (this.height[rootP] < this.height[rootQ]) { this.parent[rootP] = rootQ; } else if (this.height[rootP] > this.height[rootQ]) { this.parent[rootQ] = rootP; } else { this.parent[rootQ] = rootP; this.height[rootP]++; } this.count--; } } }
1.5.15
解答
首先證明在最壞情況下加權 quick-union 算法生成的樹中的每一層結點數均為二項式系數。
最壞情況下,每次 union 操作都是合並相同大小的樹,如下圖所示:
設第 i 層的結點數為 ki,那么最壞情況下每次合並后的 ki’ = ki + ki-1 。
這符合二項式系數的構造特點(詳情可以搜索楊輝三角),第一個結論證明完畢。
接下來求平均深度,首先根據二項式的求和公式,一棵深度為 n 的樹(根結點的深度為零)結點總數為:
每層結點數 × 該層深度后的和為:
這里用到了這個公式化簡:
相除可以求得平均深度:
1.5.16
解答
給出繪圖結果樣例:
代碼
僅給出繪圖相關的代碼,窗體部分見 github 上的代碼:
using System; using System.Linq; using System.Windows.Forms; using System.Drawing; using UnionFind; namespace _1._5._16 { /* * 1.5.16 * * 均攤成本的圖像。 * 修改你為練習 1.5.7 給出的實現, * 繪出如正文所示的均攤成本的圖像。 * */ static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Compute(); Application.Run(new Form1()); } static void Compute() { char[] split = { '\n', '\r' }; string[] input = TestCase.Properties.Resources.mediumUF.Split(split, StringSplitOptions.RemoveEmptyEntries); int size = int.Parse(input[0]); QuickFindUF quickFind = new QuickFindUF(size); QuickUnionUF quickUnion = new QuickUnionUF(size); string[] pair; int p, q; int[] quickFindResult = new int[size]; int[] quickUnionResult = new int[size]; for (int i = 1; i < size; ++i) { pair = input[i].Split(' '); p = int.Parse(pair[0]); q = int.Parse(pair[1]); quickFind.Union(p, q); quickUnion.Union(p, q); quickFindResult[i - 1] = quickFind.ArrayVisitCount; quickUnionResult[i - 1] = quickUnion.ArrayVisitCount; quickFind.ResetArrayCount(); quickUnion.ResetArrayCount(); } Draw(quickFindResult); Draw(quickUnionResult); } static void Draw(int[] cost) { // 構建 total 數組。 int[] total = new int[cost.Length]; total[0] = cost[0]; for (int i = 1; i < cost.Length; ++i) { total[i] = total[i - 1] + cost[i]; } // 獲得最大值。 int costMax = cost.Max(); // 新建繪圖窗口。 Form2 plot = new Form2(); plot.Show(); Graphics graphics = plot.CreateGraphics(); // 獲得繪圖區矩形。 RectangleF rect = plot.ClientRectangle; float unitX = rect.Width / 10; float unitY = rect.Width / 10; // 添加 10% 邊距作為文字區域。 RectangleF center = new RectangleF (rect.X + unitX, rect.Y + unitY, rect.Width - 2 * unitX, rect.Height - 2 * unitY); // 繪制坐標系。 graphics.DrawLine(Pens.Black, center.Left, center.Top, center.Left, center.Bottom); graphics.DrawLine(Pens.Black, center.Left, center.Bottom, center.Right, center.Bottom); graphics.DrawString(costMax.ToString(), plot.Font, Brushes.Black, rect.Location); graphics.DrawString(cost.Length.ToString(), plot.Font, Brushes.Black, center.Right, center.Bottom); graphics.DrawString("0", plot.Font, Brushes.Black, rect.Left, center.Bottom); // 初始化點。 PointF[] grayPoints = new PointF[cost.Length]; PointF[] redPoints = new PointF[cost.Length]; unitX = center.Width / cost.Length; unitY = center.Width / costMax; for (int i = 0; i < cost.Length; ++i) { grayPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - (cost[i] * unitY)); redPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - ((total[i] / (i + 1)) * unitY)); } // 繪制點。 for (int i = 0; i < cost.Length; ++i) { graphics.DrawEllipse(Pens.Gray, new RectangleF(grayPoints[i], new SizeF(2, 2))); graphics.DrawEllipse(Pens.Red, new RectangleF(redPoints[i], new SizeF(2, 2))); } graphics.Dispose(); } } }
1.5.17
解答
官方給出的 ErdosRenyi:ErdosRenyi.java。
為了方便之后做題,除了 Count() 之外,這個類還包含其他方法,具體可以查看注釋。
代碼
ErdosRenyi.cs
using System; using System.Collections.Generic; namespace UnionFind { /// <summary> /// 提供一系列對並查集進行隨機測試的靜態方法。 /// </summary> public class ErdosRenyi { /// <summary> /// 隨機生成一組能讓並查集只剩一個連通分量的連接。 /// </summary> /// <param name="n">並查集大小。</param> /// <returns>一組能讓並查集只剩一個連通分量的連接。</returns> public static Connection[] Generate(int n) { Random random = new Random(); List<Connection> connections = new List<Connection>(); WeightedQuickUnionPathCompressionUF uf = new WeightedQuickUnionPathCompressionUF(n); while (uf.Count() > 1) { int p = random.Next(n); int q = random.Next(n); uf.Union(p, q); connections.Add(new Connection(p, q)); } return connections.ToArray(); } /// <summary> /// 隨機生成連接,返回令並查集中只剩一個連通分量所需的連接總數。 /// </summary> /// <param name="uf">用於測試的並查集。</param> /// <returns>需要的連接總數。</returns> public static int Count(UF uf) { Random random = new Random(); int size = uf.Count(); int edges = 0; while (uf.Count() > 1) { int p = random.Next(size); int q = random.Next(size); uf.Union(p, q); edges++; } return edges; } /// <summary> /// 使用指定的連接按順序合並。 /// </summary> /// <param name="uf">需要測試的並查集。</param> /// <param name="connections">用於輸入的連接集合。</param> public static void Count(UF uf, Connection[] connections) { foreach (Connection c in connections) { uf.Union(c.P, c.Q); } } } }
Main 方法:
using System; using UnionFind; namespace _1._5._17 { /* * 1.5.17 * * 隨機鏈接。 * 設計 UF 的一個用例 ErdosRenyi, * 從命令行接受一個整數 N,在 0 到 N-1 之間產生隨機整數對, * 調用 connected() 判斷它們是否相連, * 如果不是則調用 union() 方法(和我們的開發用例一樣)。 * 不斷循環直到所有觸點均相互連通並打印出生成的連接總數。 * 將你的程序打包成一個接受參數 N 並返回連接總數的靜態方法 count(), * 添加一個 main() 方法從命令行接受 N,調用 count() 並打印它的返回值。 * */ class Program { static void Main(string[] args) { int N = 10; int[] edges = new int[5]; for (int i = 0; i < 5; ++i) { var uf = new UF(N); Console.WriteLine(N + "\t" + ErdosRenyi.Count(uf)); N *= 10; } } } }
1.5.18
解答
具體生成的連接樣式見下題,這里給出 RandomGrid 的實現,需要使用 1.3 節中的隨機背包輔助。
代碼
RandomGrid.cs
using System; using System.Collections.Generic; namespace UnionFind { public class RandomGrid { /// <summary> /// 隨機生成 n × n 網格中的所有連接。 /// </summary> /// <param name="n">網格邊長。</param> /// <returns>隨機排序的連接。</returns> public static RandomBag<Connection> Generate(int n) { var result = new RandomBag<Connection>(); var random = new Random(); // 建立橫向連接 for (int i = 0; i < n; ++i) { for (int j = 0; j < n - 1; ++j) { if (random.Next(10) > 4) { result.Add(new Connection(i * n + j, (i * n) + j + 1)); } else { result.Add(new Connection((i * n) + j + 1, i * n + j)); } } } // 建立縱向連接 for (int j = 0; j < n; ++j) { for (int i = 0; i < n - 1; ++i) { if (random.Next(10) > 4) { result.Add(new Connection(i * n + j, ((i + 1) * n) + j)); } else { result.Add(new Connection(((i + 1) * n) + j, i * n + j)); } } } return result; } /// <summary> /// 隨機生成 n × n 網格中的所有連接,返回一個連接數組。 /// </summary> /// <param name="n">網格邊長。</param> /// <returns>連接數組。</returns> public static Connection[] GetConnections(int n) { RandomBag<Connection> bag = Generate(n); List<Connection> connections = new List<Connection>(); foreach (Connection c in bag) { connections.Add(c); } return connections.ToArray(); } } }
1.5.19
解答
最后繪出的圖像:
代碼
給出繪圖部分的代碼,窗體部分見 GitHub。
using System; using System.Drawing; using System.Collections.Generic; using System.Windows.Forms; using UnionFind; namespace _1._5._19 { /* * 1.5.19 * * 動畫。 * 編寫一個 RandomGrid(請見練習 1.5.18)的用例, * 和我們開發用例一樣使用 UnionFind 來檢查觸點的連通性並在處理時用 StdDraw 將它們繪出。 * */ static class Program { static RandomBag<Connection> bag; static Graphics graphics; static TextBox logBox; static PointF[] points; static Timer timer; static List<Connection> connections; static int count = 0; /// <summary> /// 應用程序的主入口點。 /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } /// <summary> /// 繪制連接圖像。 /// </summary> /// <param name="n">矩陣邊長。</param> public static void Draw(int n, TextBox log, Log WinBox) { logBox = log; // 生成路徑。 log.AppendText("\r\n開始生成連接……"); bag = RandomGrid.Generate(n); log.AppendText("\r\n生成連接完成"); // 新建畫布窗口。 log.AppendText("\r\n啟動畫布……"); Form2 matrix = new Form2(); matrix.StartPosition = FormStartPosition.Manual; matrix.Location = new Point(WinBox.Left - matrix.ClientRectangle.Width, WinBox.Top); matrix.Show(); log.AppendText("\r\n畫布已啟動,開始繪圖……"); graphics = matrix.CreateGraphics(); // 獲取繪圖區域。 RectangleF rect = matrix.ClientRectangle; float unitX = rect.Width / (n + 1); float unitY = rect.Height / (n + 1); // 繪制點。 log.AppendText("\r\n繪制點……"); points = new PointF[n * n]; for (int row = 0; row < n; ++row) { for (int col = 0; col < n; ++col) { points[row * n + col] = new PointF(unitX * (col + 1), unitY * (row + 1)); graphics.FillEllipse(Brushes.Black, unitX * (col + 1), unitY * (row + 1), 5, 5); } } log.AppendText("\r\n繪制點完成"); // 繪制連接。 log.AppendText("\r\n開始繪制連接……"); connections = new List<Connection>(); foreach (Connection c in bag) { connections.Add(c); } timer = new Timer { Interval = 500 }; timer.Tick += DrawOneLine; timer.Start(); } private static void DrawOneLine(object sender, EventArgs e) { Connection c = connections[count]; count++; graphics.DrawLine(Pens.Black, points[c.P], points[c.Q]); logBox.AppendText("\r\n繪制" + "(" + c.P + ", " + c.Q + ")"); if (count == bag.Size()) { timer.Stop(); logBox.AppendText("\r\n繪制結束"); count = 0; } } } }
1.5.20
解答
將 parent 數組和 size 數組用鏈表代替即可,很容易實現。
代碼
修改后的 WeightedQuickUnionUF.cs
using System; namespace _1._5._20 { /// <summary> /// 使用加權 quick-union 算法的並查集。 /// </summary> public class WeightedQuickUnionUF { protected LinkedList<int> parent; // 記錄各個結點的父級。 protected LinkedList<int> size; // 記錄各個樹的大小。 protected int count; // 分量數目。 /// <summary> /// 建立使用加權 quick-union 的並查集。 /// </summary> /// <param name="n">並查集的大小。</param> public WeightedQuickUnionUF() { this.parent = new LinkedList<int>(); this.size = new LinkedList<int>(); } /// <summary> /// 獲取 parent 數組。 /// </summary> /// <returns>parent 數組。</returns> public LinkedList<int> GetParent() { return this.parent; } /// <summary> /// 獲取 size 數組。 /// </summary> /// <returns>返回 size 數組。</returns> public LinkedList<int> GetSize() { return this.size; } /// <summary> /// 在並查集中增加一個新的結點。 /// </summary> /// <returns>新結點的下標。</returns> public int NewSite() { this.parent.Insert(this.parent.Size(), this.parent.Size()); this.size.Insert(1, this.size.Size()); this.count++; return this.parent.Size() - 1; } /// <summary> /// 尋找一個結點所在的連通分量。 /// </summary> /// <param name="p">需要尋找的結點。</param> /// <returns>該結點所屬的連通分量。</returns> public int Find(int p) { Validate(p); while (p != this.parent.Find(p)) { p = this.parent.Find(p); } return p; } /// <summary> /// 將兩個結點所屬的連通分量合並。 /// </summary> /// <param name="p">需要合並的結點。</param> /// <param name="q">需要合並的另一個結點。</param> public void Union(int p, int q) { int rootP = Find(p); int rootQ = Find(q); if (rootP == rootQ) { return; } if (this.size.Find(rootP) < this.size.Find(rootQ)) { this.parent.Motify(rootP, rootQ); this.size.Motify(rootQ, this.size.Find(rootQ) + this.size.Find(rootP)); } else { this.parent.Motify(rootQ, rootP); this.size.Motify(rootP, this.size.Find(rootQ) + this.size.Find(rootP)); } this.count--; } /// <summary> /// 檢查輸入的 p 是否符合條件。 /// </summary> /// <param name="p">輸入的 p 值。</param> protected void Validate(int p) { int n = this.parent.Size(); if (p < 0 || p >= n) { throw new ArgumentException("index" + p + " is not between 0 and " + (n - 1)); } } } }
1.5.21
解答
給出我電腦上的結果:
實驗結果:10 1/2NlnN:11.5129254649702 實驗結果:29 1/2NlnN:29.9573227355399 實驗結果:132 1/2NlnN:73.7775890822787 實驗結果:164 1/2NlnN:175.281065386955 實驗結果:418 1/2NlnN:406.013905218706 實驗結果:1143 1/2NlnN:922.931359327004 實驗結果:2004 1/2NlnN:2067.66981643319 實驗結果:4769 1/2NlnN:4578.95382842474 實驗結果:10422 1/2NlnN:10045.1360479662 實驗結果:21980 1/2NlnN:21864.7288781659
代碼
using System; using UnionFind; namespace _1._5._21 { /* * 1.5.21 * * Erdös-Renyi 模型。 * 使用練習 1.5.17 的用例驗證這個猜想: * 得到單個連通分量所需生成的整數對數量為 ~1/2NlnN。 * */ class Program { static void Main(string[] args) { for (int n = 10; n < 10000; n *= 2) { int total = 0; for (int i = 0; i < 100; ++i) { UF uf = new UF(n); total += ErdosRenyi.Count(uf); } Console.WriteLine("實驗結果:" + total / 100); Console.WriteLine("1/2NlnN:" + Math.Log(n) * n * 0.5); Console.WriteLine(); } } } }
1.5.22
解答
平方級別算法在輸入加倍后耗時應該增加四倍,線性則是兩倍。
下面給出我電腦上的結果,數據量較大時比較明顯:
N:16000 quick-find 平均次數:8452 用時:143 比值:4.46875 quick-union 平均次數:7325 用時:202 比值:3.25806451612903 weighted-quick-union 平均次數:6889 用時:1 N:32000 quick-find 平均次數:15747 用時:510 比值:3.56643356643357 quick-union 平均次數:15108 用時:801 比值:3.96534653465347 weighted-quick-union 平均次數:17575 用時:3 比值:3 N:64000 quick-find 平均次數:33116 用時:2069 比值:4.05686274509804 quick-union 平均次數:38608 用時:4635 比值:5.78651685393258 weighted-quick-union 平均次數:34850 用時:6 比值:2
代碼
using System; using System.Diagnostics; using UnionFind; namespace _1._5._22 { /* * 1.5.22 * * Erdös-Renyi 的倍率實驗。 * 開發一個性能測試用例, * 從命令行接受一個 int 值 T 並進行 T 次以下實驗: * 使用練習 1.5.17 的用例生成隨機連接, * 和我們的開發用例一樣使用 UnionFind 來檢查觸點的連通性, * 不斷循環知道所有觸點都相互連通。 * 對於每個 N,打印出 N 值和平均所需的連接數以及前后兩次運行時間的比值。 * 使用你的程序驗證正文中的猜想: * quick-find 算法和 quick-union 算法的運行時間是平方級別的, * 加權 quick-union 算法則接近線性級別。 * */ class Program { static void Main(string[] args) { long lastTimeQuickFind = 0; long lastTimeQuickUnion = 0; long lastTimeWeightedQuickUnion = 0; long nowTime = 0; for (int n = 2000; n < 100000; n *= 2) { Console.WriteLine("N:" + n); QuickFindUF quickFindUF = new QuickFindUF(n); QuickUnionUF quickUnionUF = new QuickUnionUF(n); WeightedQuickUnionUF weightedQuickUnionUF = new WeightedQuickUnionUF(n); // quick-find Console.WriteLine("quick-find"); nowTime = RunTest(quickFindUF); if (lastTimeQuickFind == 0) { Console.WriteLine("用時:" + nowTime); lastTimeQuickFind = nowTime; } else { Console.WriteLine("用時:" + nowTime + " 比值:" + (double)nowTime / lastTimeQuickFind); lastTimeQuickFind = nowTime; } Console.WriteLine(); // quick-union Console.WriteLine("quick-union"); nowTime = RunTest(quickUnionUF); if (lastTimeQuickUnion == 0) { Console.WriteLine("用時:" + nowTime); lastTimeQuickUnion = nowTime; } else { Console.WriteLine("用時:" + nowTime + " 比值:" + (double)nowTime / lastTimeQuickUnion); lastTimeQuickUnion = nowTime; } Console.WriteLine(); // weighted-quick-union Console.WriteLine("weighted-quick-union"); nowTime = RunTest(weightedQuickUnionUF); if (lastTimeWeightedQuickUnion == 0) { Console.WriteLine("用時:" + nowTime); lastTimeWeightedQuickUnion = nowTime; } else { Console.WriteLine("用時:" + nowTime + " 比值:" + (double)nowTime / lastTimeWeightedQuickUnion); lastTimeWeightedQuickUnion = nowTime; } Console.WriteLine(); Console.WriteLine(); } } /// <summary> /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。 /// </summary> /// <param name="uf">用於測試的並查集。</param> /// <returns>平均耗時。</returns> static long RunTest(UF uf) { Stopwatch timer = new Stopwatch(); int total = 0; int repeatTime = 10; timer.Start(); for (int i = 0; i < repeatTime; ++i) { total += ErdosRenyi.Count(uf); } timer.Stop(); Console.WriteLine("平均次數:" + total / repeatTime); return timer.ElapsedMilliseconds / repeatTime; } } }
1.5.23
解答
先用速度最快的 WeightedQuickUnionUF 生成一系列連接,保存后用這些連接進行測試,生成連接的方法見 1.5.17 的解答。
下面給出我電腦上的結果:
N:2000 quick-find 耗時(毫秒):4 quick-union 耗時(毫秒):5 比值:0.8 N:4000 quick-find 耗時(毫秒):19 quick-union 耗時(毫秒):24 比值:0.791666666666667 N:8000 quick-find 耗時(毫秒):57 quick-union 耗時(毫秒):74 比值:0.77027027027027 N:16000 quick-find 耗時(毫秒):204 quick-union 耗時(毫秒):307 比值:0.664495114006515 N:32000 quick-find 耗時(毫秒):1127 quick-union 耗時(毫秒):1609 比值:0.700435052827843
代碼
using System; using System.Diagnostics; using UnionFind; namespace _1._5._23 { /* * 1.5.23 * * 在 Erdös-Renyi 模型下比較 quick-find 算法和 quick-union 算法。 * 開發一個性能測試用例,從命令行接受一個 int 值 T 並進行 T 次以下實驗: * 使用練習 1.5.17 的用例生成隨機連接。 * 保存這些連接並和我們的開發用例一樣分別用 quick-find 和 quick-union 算法檢查觸點的連通性, * 不斷循環直到所有觸點均相互連通。 * 對於每個 N,打印出 N 值和兩種算法的運行時間比值。 * */ class Program { static void Main(string[] args) { int n = 1000; for (int t = 0; t < 5; ++t) { Connection[] input = ErdosRenyi.Generate(n); QuickFindUF quickFind = new QuickFindUF(n); QuickUnionUF quickUnion = new QuickUnionUF(n); Console.WriteLine("N:" + n); long quickFindTime = RunTest(quickFind, input); long quickUnionTime = RunTest(quickUnion, input); Console.WriteLine("quick-find 耗時(毫秒):" + quickFindTime); Console.WriteLine("quick-union 耗時(毫秒):" + quickUnionTime); Console.WriteLine("比值:" + (double)quickFindTime / quickUnionTime); Console.WriteLine(); n *= 2; } } /// <summary> /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。 /// </summary> /// <param name="uf">用於測試的並查集。</param> /// <param name="connections">用於測試的輸入。</param> /// <returns>平均耗時。</returns> static long RunTest(UF uf, Connection[] connections) { Stopwatch timer = new Stopwatch(); int repeatTime = 5; timer.Start(); for (int i = 0; i < repeatTime; ++i) { ErdosRenyi.Count(uf, connections); } timer.Stop(); return timer.ElapsedMilliseconds / repeatTime; } } }
1.5.24
解答
根據上題的代碼略作修改即可,路徑壓縮大概可以快 1/3。
N:10000 加權 quick-find 耗時(毫秒):9 帶路徑壓縮的加權 quick-union 耗時(毫秒):6 比值:1.5 N:20000 加權 quick-find 耗時(毫秒):12 帶路徑壓縮的加權 quick-union 耗時(毫秒):8 比值:1.5 N:40000 加權 quick-find 耗時(毫秒):18 帶路徑壓縮的加權 quick-union 耗時(毫秒):12 比值:1.5 N:80000 加權 quick-find 耗時(毫秒):36 帶路徑壓縮的加權 quick-union 耗時(毫秒):30 比值:1.2 N:160000 加權 quick-find 耗時(毫秒):67 帶路徑壓縮的加權 quick-union 耗時(毫秒):41 比值:1.63414634146341
代碼
using System; using UnionFind; using System.Diagnostics; namespace _1._5._24 { /* * 1.5.24 * * 適用於 Erdös-Renyi 模型的快速算法。 * 在練習1.5.23 的測試中增加加權 quick-union 算法和使用路徑壓縮的加權 quick-union 算法。 * 你能分辨出這兩種算法的區別嗎? * */ class Program { static void Main(string[] args) { int n = 5000; for (int t = 0; t < 5; ++t) { var input = ErdosRenyi.Generate(n); var weightedQuickUnionUF = new WeightedQuickUnionUF(n); var weightedQuickUnionPathCompressionUF = new WeightedQuickUnionPathCompressionUF(n); Console.WriteLine("N:" + n); long weightedQuickUnionTime = RunTest(weightedQuickUnionUF, input); long weightedQuickUnionPathCompressionTime = RunTest(weightedQuickUnionPathCompressionUF, input); Console.WriteLine("加權 quick-find 耗時(毫秒):" + weightedQuickUnionTime); Console.WriteLine("帶路徑壓縮的加權 quick-union 耗時(毫秒):" + weightedQuickUnionPathCompressionTime); Console.WriteLine("比值:" + (double)weightedQuickUnionTime / weightedQuickUnionPathCompressionTime); Console.WriteLine(); n *= 2; } } /// <summary> /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。 /// </summary> /// <param name="uf">用於測試的並查集。</param> /// <param name="connections">用於測試的輸入。</param> /// <returns>平均耗時。</returns> static long RunTest(UF uf, Connection[] connections) { Stopwatch timer = new Stopwatch(); int repeatTime = 5; timer.Start(); for (int i = 0; i < repeatTime; ++i) { ErdosRenyi.Count(uf, connections); } timer.Stop(); return timer.ElapsedMilliseconds / repeatTime; } } }
1.5.25
解答
略微修改 1.5.22 的代碼即可。
我電腦上的結果:
Quick-Find N:1600 平均用時(毫秒):4 N:6400 平均用時(毫秒):67 比值:16.75 N:25600 平均用時(毫秒):1268 比值:18.9253731343284 N:102400 平均用時(毫秒):20554 比值:16.2097791798107 Quick-Union N:1600 平均用時(毫秒):5 比值:0.000243261652233142 N:6400 平均用時(毫秒):66 比值:13.2 N:25600 平均用時(毫秒):1067 比值:16.1666666666667 N:102400 平均用時(毫秒):18637 比值:17.4667291471415 Weighted Quick-Union N:1600 平均用時(毫秒):0 比值:0 N:6400 平均用時(毫秒):2 N:25600 平均用時(毫秒):12 比值:6 N:102400 平均用時(毫秒):64 比值:5.33333333333333
代碼
using System; using System.Diagnostics; using UnionFind; namespace _1._5._25 { /* * 1.5.25 * * 隨機網格的倍率測試。 * 開發一個性能測試用例, * 從命令行接受一個 int 值 T 並進行 T 次以下實驗: * 使用練習 1.5.18 的用例生成一個 N×N 的隨機網格, * 所有連接的方向隨機且排列隨機。 * 和我們的開發用例一樣使用 UnionFind 來檢查觸點的連通性, * 不斷循環直到所有觸點均相互連通。 * 對於每個 N,打印出 N 值和平均所需的連接數以及前后兩次運行時間的比值。 * 使用你的程序驗證正文中的猜想: * quick-find 和 quick-union 算法的運行時間是平方級別的, * 加權 quick-union 算法則接近線性級別。 * * 注意:隨着 N 值加倍,網格中觸點的數量會乘以 4, * 因此平方級別的算法運行時間會變為原來的 16 倍, * 線性級別的算法的運行時間則變為原來的 4 倍 * */ class Program { static void Main(string[] args) { int n = 40; int t = 4; // quick-find Console.WriteLine("Quick-Find"); long last = 0; long now = 0; for (int i = 0; i < t; ++i, n *= 2) { Console.WriteLine("N:" + n * n); var connections = RandomGrid.GetConnections(n); QuickFindUF quickFind = new QuickFindUF(n * n); now = RunTest(quickFind, connections); if (last == 0) { Console.WriteLine("平均用時(毫秒):" + now); last = now; } else { Console.WriteLine("平均用時(毫秒):" + now + "\t比值:" + (double)now / last); last = now; } } // quick-union Console.WriteLine("Quick-Union"); n = 40; for (int i = 0; i < t; ++i, n *= 2) { Console.WriteLine("N:" + n * n); var connections = RandomGrid.GetConnections(n); QuickUnionUF quickFind = new QuickUnionUF(n * n); now = RunTest(quickFind, connections); if (last == 0) { Console.WriteLine("平均用時(毫秒):" + now); last = now; } else { Console.WriteLine("平均用時(毫秒):" + now + "\t比值:" + (double)now / last); last = now; } } // 加權 quick-union Console.WriteLine("Weighted Quick-Union"); n = 40; for (int i = 0; i < t; ++i, n *= 2) { Console.WriteLine("N:" + n * n); var connections = RandomGrid.GetConnections(n); WeightedQuickUnionUF quickFind = new WeightedQuickUnionUF(n * n); now = RunTest(quickFind, connections); if (last == 0) { Console.WriteLine("平均用時(毫秒):" + now); last = now; } else { Console.WriteLine("平均用時(毫秒):" + now + "\t比值:" + (double)now / last); last = now; } } } /// <summary> /// 進行若干次隨機試驗,輸出平均 union 次數,返回平均耗時。 /// </summary> /// <param name="uf">用於測試的並查集。</param> /// <param name="connections">用於測試的輸入。</param> /// <returns>平均耗時。</returns> static long RunTest(UF uf, Connection[] connections) { Stopwatch timer = new Stopwatch(); long repeatTime = 3; timer.Start(); for (int i = 0; i < repeatTime; ++i) { ErdosRenyi.Count(uf, connections); } timer.Stop(); return timer.ElapsedMilliseconds / repeatTime; } } }
1.5.26
解答
和 1.5.16 的程序類似,將測試的內容改為 Erdos-Renyi 即可。
樣例輸出:
代碼
using System; using System.Linq; using System.Windows.Forms; using System.Drawing; using UnionFind; namespace _1._5._26 { /* * 1.5.26 * * Erdös-Renyi 模型的均攤成本圖像。 * 開發一個用例, * 從命令行接受一個 int 值 N,在 0 到 N-1 之間產生隨機整數對, * 調用 connected() 判斷它們是否相連, * 如果不是則用 union() 方法(和我們的開發用例一樣)。 * 不斷循環直到所有觸點互通。 * 按照正文的樣式將所有操作的均攤成本繪制成圖像。 * */ static class Program { /// <summary> /// 應用程序的主入口點。 /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Compute(); Application.Run(new Form1()); } static void Compute() { int size = 200; QuickFindUF quickFind = new QuickFindUF(size); QuickUnionUF quickUnion = new QuickUnionUF(size); WeightedQuickUnionUF weightedQuickUnion = new WeightedQuickUnionUF(size); Connection[] connections = ErdosRenyi.Generate(size); int[] quickFindResult = new int[size]; int[] quickUnionResult = new int[size]; int[] weightedQuickUnionResult = new int[size]; int p, q; for (int i = 0; i < size; ++i) { p = connections[i].P; q = connections[i].Q; quickFind.Union(p, q); quickUnion.Union(p, q); weightedQuickUnion.Union(p, q); quickFindResult[i] = quickFind.ArrayVisitCount; quickUnionResult[i] = quickUnion.ArrayVisitCount; weightedQuickUnionResult[i] = weightedQuickUnion.ArrayParentVisitCount + weightedQuickUnion.ArraySizeVisitCount; quickFind.ResetArrayCount(); quickUnion.ResetArrayCount(); weightedQuickUnion.ResetArrayCount(); } Draw(quickFindResult, "Quick-Find"); Draw(quickUnionResult, "Quick-Union"); Draw(weightedQuickUnionResult, "Weighted Quick-Union"); } static void Draw(int[] cost, string title) { // 構建 total 數組。 int[] total = new int[cost.Length]; total[0] = cost[0]; for (int i = 1; i < cost.Length; ++i) { total[i] = total[i - 1] + cost[i]; } // 獲得最大值。 int costMax = cost.Max(); // 新建繪圖窗口。 Form2 plot = new Form2(); plot.Text = title; plot.Show(); Graphics graphics = plot.CreateGraphics(); // 獲得繪圖區矩形。 RectangleF rect = plot.ClientRectangle; float unitX = rect.Width / 10; float unitY = rect.Width / 10; // 添加 10% 邊距作為文字區域。 RectangleF center = new RectangleF (rect.X + unitX, rect.Y + unitY, rect.Width - 2 * unitX, rect.Height - 2 * unitY); // 繪制坐標系。 graphics.DrawLine(Pens.Black, center.Left, center.Top, center.Left, center.Bottom); graphics.DrawLine(Pens.Black, center.Left, center.Bottom, center.Right, center.Bottom); graphics.DrawString(costMax.ToString(), plot.Font, Brushes.Black, rect.Location); graphics.DrawString(cost.Length.ToString(), plot.Font, Brushes.Black, center.Right, center.Bottom); graphics.DrawString("0", plot.Font, Brushes.Black, rect.Left, center.Bottom); // 初始化點。 PointF[] grayPoints = new PointF[cost.Length]; PointF[] redPoints = new PointF[cost.Length]; unitX = center.Width / cost.Length; unitY = center.Width / costMax; for (int i = 0; i < cost.Length; ++i) { grayPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - (cost[i] * unitY)); redPoints[i] = new PointF(center.Left + unitX * (i + 1), center.Bottom - ((total[i] / (i + 1)) * unitY)); } // 繪制點。 for (int i = 0; i < cost.Length; ++i) { graphics.FillEllipse(Brushes.Gray, new RectangleF(grayPoints[i], new SizeF(5, 5))); graphics.FillEllipse(Brushes.Red, new RectangleF(redPoints[i], new SizeF(5, 5))); } graphics.Dispose(); } } }