算法的主題思想:算法
1.優秀的算法由於可以解決實際問題而變得更爲重要;數組
2.高效算法的代碼也能夠很簡單;網絡
3.理解某個實現的性能特色是一個挑戰;數據結構
4.在解決同一個問題的多種算法之間進行選擇時,科學方法是一種重要的工具;工具
5.迭代式改進可以讓算法的效率愈來愈高效;性能
1. 動態連通性ui
動態鏈接:輸入是一對整數對的序列,其中每一個整數表明某種類型的對象(或觸點),咱們將整數對p q 解釋爲意味着p鏈接到q。咱們假設「鏈接到」是等價關係:spa
對稱性:若是p鏈接到q,則q 鏈接到p。設計
傳遞性:若是p鏈接到q且q 鏈接到r,則p鏈接到r。
自反性:p與p鏈接。
等價關係將對象劃分爲多個等價類 或鏈接的組件。等價類稱爲連通份量或份量。
咱們的目標是編寫一個程序,以從序列中過濾掉多餘的對:當程序從輸入中讀取整數對 p q時,只有在該對點不等價的狀況下,才應將對寫入到輸出中,而且將p鏈接到q。若是等價,則程序應忽略整數對pq 並繼續讀取下對。code
動態連通性問題的應用:
1.網絡
2.變量名等價性
3.數學集合
在更高的抽象層次上,能夠將輸入的全部整數看作屬於不一樣的數學集合。
2. 定義問題
設計算法的第一個任務就是精確地定義問題。
算法解決的問題越大,它完成任務所需的時間和空間可能越多。咱們不可能預先知道這其間的量化關係,一般只會在發現解決問題很困難,或是代價巨大,或是發現算法所提供的信息比原問題所須要的更加有用時修改問題。例如,連通性問題只要求咱們的程序可以判斷出給定的整數對是否相連,但並無要求給出二者之間的通路上的全部鏈接。這樣的要求更難,並會得出另外一組不一樣的算法。
爲了定義和說明問題,先設計一份API 來封裝基本操做: 初始化,鏈接兩個觸點,查找某個觸點的份量 ,判斷兩個觸點是否屬於同一份量,份量的數量:
/// <summary> /// 動態連通API /// </summary> public interface IUnionFind { /// <summary> /// 鏈接 /// </summary> /// <param name="p"></param> /// <param name="q"></param> void Union(int p, int q); /// <summary> /// 查找觸點 p 的份量標識符 /// </summary> /// <param name="p"></param> /// <returns></returns> int Find(int p); /// <summary> /// 判斷兩個觸點是否處於同一份量 /// </summary> /// <param name="p"></param> /// <param name="q"></param> /// <returns></returns> bool Connected(int p, int q); /// <summary> /// 連通份量的數量 /// </summary> /// <returns></returns> int Count(); }
爲解決動態連通性問題設計算法的任務轉化爲實現這份API:
1. 定義一種數據結構表示已知的鏈接;
2. 基於此數據結構高效的實現API的方法;
數據結構的性質會直接影響算法的效率。這裏,以觸點爲索引,觸點和鏈接份量都是用 int 值表示,將會使用份量中某個觸點的值做爲份量的標識符。因此,一開始,每一個觸點都是隻含有本身的份量,份量標識符爲觸點的值。由此,能夠初步實現一部分方法:
public class FirstUnionFind:IUnionFind { private int[] id;//* 份量id 以觸點做爲索引 private int count;//份量數量 public FirstUnionFind(int n) { count = n; id = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一個 i 做爲觸點,第二個 i 做爲觸點的值 } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { } public void Union(int p, int q) { } }
Union-find 的成本模型 是數組的訪問次數(不管讀寫)。
3. quick-find算法實現
quick-find 算法是保證當且僅當 id[p] 等於 id[q] 時,p 和 q 是連通的。也就是說,在同一個連通份量中的全部觸點在 id[ ] 中的值所有相等。
因此 Find 方法只需返回 id[q],Union 方法須要先判斷 Find(p) 是否等於 Find(q) ,若相等直接返回;若不相等,須要將 q 所在的連通份量中全部觸點的 id [ ] 值所有更新爲 id[p]。
public class QuickFindUF: IUnionFind { private int[] id;//* 份量id 以觸點做爲索引 private int count;//份量數量 public QuickFindUF(int n) { count = n; id = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一個 i 做爲觸點,第二個 i 做爲觸點的值 } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { return id[p]; } public void Union(int p, int q) { var pID = Find(p); var qID = Find(q); if (pID == qID) return; for (var i = 0; i < id.Length; i++) { if (id[i] == qID) id[i] = pID; } count--; //連通份量減小 } public void Show() { for(var i = 0;i<id.Length;i++) Console.WriteLine("索引:"+i+",值:"+ id[i] ); Console.WriteLine("連通份量數量:"+count); } }
算法分析
Find() 方法只需訪問一次數組,因此速度很快。可是對於處理大型問題,每對輸入 Union() 方法都須要掃描整個數組。
每一次歸併兩個份量的 Union() 方法訪問數組的次數在 N+3 到 2N+1 之間。由代碼可知,兩次 Find 操做訪問兩次數組,掃描數組會訪問N次,改變其中一個份量中全部觸點的值須要訪問 1 到 N - 1 次(最好狀況是該份量中只有一個觸點,最壞狀況是該份量中有 N - 1個觸點),2+N+N-1。
若是使用quick-find 算法來解決動態連通性問題而且最後只獲得一個連通份量,至少須要調用 N-1 次Union() 方法,那麼至少須要 (N+3)(N-1) ~ N^2 次訪問數組,是平方級別的。
4. quick-union算法實現
quick-union 算法重點提升 union 方法的速度,它也是基於相同的數據結構 -- 已觸點爲索引的 id[ ] 數組,可是 id[ ] 的值是同一份量中另外一觸點的索引(名稱),也多是本身(根觸點)——這種聯繫成爲連接。
在實現 Find() 方法時,從給定觸點,連接到另外一個觸點,知道到達根觸點,即連接指向本身。同時修改 Union() 方法,分別找到 p q 的根觸點,將其中一個根觸點連接到根觸點。
public class QuickUnionUF : IUnionFind { private int[] id; private int count; public QuickUnionUF(int n) { count = n; id = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一個 i 做爲觸點,第二個 i 做爲觸點的值 } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { while (p != id[p]) p = id[p]; return p; } public void Union(int p, int q) { var pRoot = Find(p); var qRoot = Find(q); if (pRoot == qRoot) return; id[pRoot] =qRoot;
count--; //連通份量減小 } public void Show() { for (var i = 0; i < id.Length; i++) Console.WriteLine("索引:" + i + ",值:" + id[i]); Console.WriteLine("連通份量數量:" + count); } }
森林表示
id[ ] 數組用父連接的形式表示一片森林,用節點表示觸點。不管從任何觸點所對應的節點隨着連接查找,最後都將到達含有該節點的根節點。初始化數組以後,每一個節點的連接都指向本身。
算法分析
定義:一棵樹的大小是它的節點的數量。樹中一個節點的深度是它到根節點的路徑上連接數。樹的高度是它的全部節點中的最大深度。
quick-union 算法比 quick-find 算法更快,由於它對每對輸入不須要遍歷整個數組。
分析quick-union 算法的成本比 quick-find 算法的成本要困難,由於quick-union 算法依賴於輸入的特色。在最好的狀況下,find() 方法只需訪問一次數組就能夠獲得一個觸點的份量表示;在最壞狀況下,須要 2i+1 次數組訪問(i 時觸點的深度)。由此得出,該算法解決動態連通性問題,在最佳狀況下的運行時間是線性級別,最壞狀況下的輸入是平方級別。解決了 quick-find 算法中 union() 方法老是線性級別,解決動態連通性問題老是平方級別。
quick-union 算法中 find() 方法訪問數組的次數爲 1(到達根節點只需訪問一次) 加上 給定觸點所對應節點的深度的兩倍(while 循環,一次讀,一次寫)。union() 訪問兩次 find() ,若是兩個觸點不在同一份量還需加一次寫數組。
假設輸入的整數對是有序的 0-1, 0-2,0-3 等,N-1 對以後N個觸點將所有處於相同的集合之中,且獲得的樹的高度爲 N-1。由上可知,對於整數對 0-i , find() 訪問數組的次數爲 2i + 1,所以,處理 N 對整數對所需的全部訪問數組的總次數爲 3+5+7+ ......+(2N+1) ~ n^2
5.加權 quick-union 算法實現
簡單改動就能夠避免 quick-union算法 出現最壞狀況。quick-union算法 union 方法是隨意將一棵樹鏈接到另外一棵樹,改成老是將小樹鏈接到大樹,這須要記錄每一棵樹的大小,稱爲加權quick-union算法。
代碼:
public class WeightedQuickUnionUF: IUnionFind { int[] sz;//以觸點爲索引的 各個根節點對應的份量樹大小 private int[] id; private int count; public WeightedQuickUnionUF(int n) { count = n; id = new int[n]; sz = new int[n]; for (var i = 0; i < n; i++) { id[i] = i; // 第一個 i 做爲觸點,第二個 i 做爲觸點的值 sz[i] = 1; } } public int Count() { return count; } public bool Connected(int p, int q) { return Find(p) == Find(q); } public int Find(int p) { while (p != id[p]) p = id[p]; return p; } public void Union(int p, int q) { var pRoot = Find(p); var qRoot = Find(q); if (pRoot == qRoot) return; if (sz[pRoot] < sz[qRoot]) { id[pRoot] = qRoot; } else { id[qRoot] = pRoot; } count--; //連通份量減小 } public void Show() { for (var i = 0; i < id.Length; i++) Console.WriteLine("索引:" + i + ",值:" + id[i]); Console.WriteLine("連通份量數量:" + count); } }
算法分析
加權 quicj-union 算法最壞的狀況:
這種狀況,將要被歸併的樹的大小老是相等的(且老是 2 的 冥),都含有 2^n 個節點,高度都正好是 n 。當歸並兩個含有 2^n 個節點的樹時,獲得的樹含有 2 ^ n+1 個節點,高度增長到 n+1 。
節點大小: 1 2 4 8 2^k = N
高 度: 0 1 2 3 k
k = logN
因此加權 quick-union 算法能夠保證對數級別的性能。
對於 N 個觸點,加權 quick-union 算法構造的森林中的任意節點的深度最多爲logN。
對於加權 quick-union 算法 和 N 個觸點,在最壞狀況下 find,connected 和 union 方法的成本的增加量級爲 logN。
對於動態連通性問題,加權 quick-union 算法 是三種算法中惟一能夠用於解決大型問題的算法。加權 quick-union 算法 處理 N 個觸點和 M 條鏈接時最多訪問數組 c M logN 次,其中 c 爲常數。
三個算法處理一百萬個觸點運行時間對比:
三個算法性能特色:
6.最優算法 - 路徑壓縮
在檢查節點的同時將它們直接鏈接到根節點。
實現:爲 find 方法添加一個循環,將在路徑上的全部節點都直接連接到根節點。徹底扁平化的樹。
研究各類基礎問題的基本步驟:
1. 完整而詳細地定義問題,找出解決問題所必須的基本抽象操做並定義一份API。
2. 簡潔地實現一種初級算法,給出一個精心組織的開發用例並使用實際數據做爲輸入。
3. 當實現所能解決的問題的最大規模達不到指望時決定改進仍是放棄。
4. 逐步改進實現,經過經驗性分析和數學分析驗證改進後的效果。
5. 用更高層次的抽象表示數據結構或算法來設計更高級的改進版本。
6. 若是可能儘可能爲最壞狀況下的性能提供保證,但在處理普通數據時也要有良好的性能。
7.在適當的時候將更細緻的深刻研究留給有經驗的研究者並解決下一個問題。