算法精解:DAG有向无环图
DAG是公認(rèn)的下一代區(qū)塊鏈的標(biāo)志。本文從算法基礎(chǔ)去研究分析DAG算法,以及它是如何運用到區(qū)塊鏈中,解決了當(dāng)前區(qū)塊鏈的哪些問題。
關(guān)鍵字:DAG,有向無環(huán)圖,算法,背包,深度優(yōu)先搜索,棧,BlockChain,區(qū)塊鏈
圖
圖是數(shù)據(jù)結(jié)構(gòu)中最為復(fù)雜的一種,我在上大學(xué)的時候,圖的這一章會被老師劃到考試范圍之外,作為我們的課后興趣部分。但實際上,圖在信息化社會中的應(yīng)用非常廣泛。圖主要包括:
- 無向圖,結(jié)點的簡單連接
- 有向圖,連接有方向性
- 加權(quán)圖,連接帶有權(quán)值
- 加權(quán)有向圖,連接既有方向性,又帶有權(quán)值
圖是由一組頂點和一組能夠?qū)蓚€頂點相連的邊組成。
常見的地圖,電路,網(wǎng)絡(luò)等都是圖的結(jié)構(gòu)。
術(shù)語
- 頂點:圖中的一個點
- 邊:連接兩個頂點的線段叫做邊,edge
- 相鄰的:一個邊的兩頭的頂點稱為是相鄰的頂點
- 度數(shù):由一個頂點出發(fā),有幾條邊就稱該頂點有幾度,或者該頂點的度數(shù)是幾,degree
- 路徑:通過邊來連接,按順序的從一個頂點到另一個頂點中間經(jīng)過的頂點集合
- 簡單路徑:沒有重復(fù)頂點的路徑
- 環(huán):至少含有一條邊,并且起點和終點都是同一個頂點的路徑
- 簡單環(huán):不含有重復(fù)頂點和邊的環(huán)
- 連通的:當(dāng)從一個頂點出發(fā)可以通過至少一條邊到達(dá)另一個頂點,我們就說這兩個頂點是連通的
- 連通圖:如果一個圖中,從任意頂點均存在一條邊可以到達(dá)另一個任意頂點,我們就說這個圖是個連通圖
- 無環(huán)圖:是一種不包含環(huán)的圖
- 稀疏圖:圖中每個頂點的度數(shù)都不是很高,看起來很稀疏
- 稠密圖:圖中的每個頂點的度數(shù)都很高,看起來很稠密
- 二分圖:可以將圖中所有頂點分為兩部分的圖
所以樹其實就是一種無環(huán)連通圖。
有向圖
有向圖是一幅有方向性的圖,由一組頂點和有向邊組成。所以,大白話來講,有向圖是包括箭頭來代表方向的。
常見的例如食物鏈,網(wǎng)絡(luò)通信等都是有向圖的結(jié)構(gòu)。
術(shù)語
上面我們介紹了頂點的度數(shù),在有向圖中,頂點被細(xì)分為了:
- 出度:由一個頂點出發(fā)的邊的總數(shù)
- 入度:指向一個頂點的邊的總數(shù)
接著,由于有向圖的方向性,一條邊的出發(fā)點稱為頭,指向點稱為尾。
- 有向路徑:圖中的一組頂點可以滿足從其中任意一個頂點出發(fā),都存在一條有向邊指向這組頂點中的另一個。
- 有向環(huán):至少含有一條邊的起點和終點都是同一個頂點的一條有向路徑。
- 簡單有向環(huán):一條不含有重復(fù)頂點和邊的環(huán)。
- 路徑或環(huán)的長度就是他們包含的邊數(shù)。
圖的連通性在有向圖中表現(xiàn)為可達(dá)性,由于邊的方向性,可達(dá)性必須是通過頂點出發(fā)的邊的正確方向,與另一個頂點可連通。
鄰接表數(shù)組
可表示圖的數(shù)據(jù)類型,意思就是如何通過一個具體的文件內(nèi)容,來表示出一幅圖的所有頂點,以及頂點間的邊。
鄰接表數(shù)組,以頂點為索引(注意頂點沒有權(quán)值,只有順序,因此是從0開始的順序值),其中每個元素都是和該頂點相鄰的頂點列表。
5 vertices, 3 edges
0: 4 1
1: 0
2:
3:
4: 背包
做一個背包集合,用來存儲與一個頂點連通的頂點集合,因為不在意存儲順序,并且只進(jìn)不出,所以選擇背包結(jié)構(gòu)來存儲。溫習(xí)一下背包
package algorithms.bag;import java.util.Iterator;// 定義一個背包集合,支持泛型,支持迭代
public class Bag<Item> implements Iterable<Item> {private class BagNode<Item> {Item item;BagNode next;}BagNode head;int size;@Overridepublic Iterator<Item> iterator() {return new Iterator<Item>() {BagNode node = head;@Overridepublic boolean hasNext() {return node.next != null;}@Overridepublic Item next() {Item item = (Item) node.item;node = node.next;return item;}};}public Bag() {head = new BagNode();size = 0;}// 往前插入public void add(Item item) {BagNode temp = new BagNode();// 以下兩行代碼一定要聲明,不可直接使用temp = head,那樣temp賦值的是head的引用,對head的所有修改會直接同步到temp,temp就不具備緩存的功能,引發(fā)bug。。temp.next = head.next;temp.item = head.item;head.item = item;head.next = temp;size++;}public boolean isEmpty() {return size == 0;}public int size() {return this.size;}public static void main(String[] args) {Bag<String> bags = new Bag();bags.add("hello");bags.add("yeah");bags.add("liu wen bin");bags.add("seminar");bags.add("1243");System.out.println(bags.size);// for (Iterator i = bags.iterator(); i.hasNext(); ) {
// System.out.println(i.next());
// }// 由于Bag實現(xiàn)了Iterable接口,所以支持以下方式遍歷for (String a : bags) {System.out.println(a);}}
}
有向圖結(jié)構(gòu)
下面代碼實現(xiàn)一個有向圖數(shù)據(jù)結(jié)構(gòu),并添加常用有向圖屬性和功能。
package algorithms.graph;import algorithms.bag.Bag;
import ioutil.In;
import ioutil.StdOut;import java.io.FileReader;public class Digraph {private final int V;// 頂點總數(shù),定義final,第一次初始化以后不可更改。private int E;// 邊總數(shù)private Bag<Integer>[] adj;// {鄰接表}頂點為數(shù)組下標(biāo),值為當(dāng)前下標(biāo)為頂點值所連通的頂點個數(shù)。public Digraph(int v) {this.V = v;this.E = 0;adj = new Bag[V];for (int i = 0; i < V; i++) {adj[i] = new Bag<Integer>();}}public Digraph(In in) {this(in.readInt());int E = in.readInt();for (int i = 0; i < E; i++) {int v = in.readInt();int w = in.readInt();addEdge(v, w);}}public int V() {return this.V;}public int E() {return this.E;}/*** v和w是兩個頂點,中間加一條邊,增加稠密度。** @param v 大V是頂點總數(shù),v是頂點值,所以并v不存在大小限制* @param w 同上。*/public void addEdge(int v, int w) {adj[v].add(w);E++;}/*** 返回一個頂點的連通頂點集合的迭代器** @param v* @return Bag本身就是迭代器,所以返回該頂點的連通頂點集合Bag即可。*/public Iterable<Integer> adj(int v) {return adj[v];}/*** 將圖中所有方向反轉(zhuǎn)** @return 返回一個圖將所有方向反轉(zhuǎn)后的副本*/public Digraph reverse() {Digraph R = new Digraph(V);for (int v = 0; v < V; v++) {for (int w : adj[v]) {// 遍歷原圖中跟v頂點連通的頂點w。R.addEdge(w, v);}}return R;}/*** 按照鄰接表數(shù)組結(jié)構(gòu)輸出有向圖內(nèi)容** @return*/public String toString() {String s = V + " vertices, " + E + " edges\n";for (int v = 0; v < V; v++) {s += v + ": ";for (int w : this.adj(v)) {s += w + " ";}s += "\n";}return s;}public static void main(String[] args) {Digraph d = new Digraph(5);d.addEdge(0, 1);d.addEdge(1, 0);d.addEdge(2, 3);d.addEdge(0, 4);StdOut.println(d);/**輸出:5 vertices, 3 edges0: 4 11: 02:3:4:*/}
}
以上背包和有向圖代碼相關(guān)解釋請具體參照代碼中注釋。
可達(dá)性
上面提到了有向圖中的可達(dá)性和圖中的連通性的關(guān)系,可達(dá)性是連通性的特殊形式,對方向敏感,所以提到有向圖,不可不研究可達(dá)性。
可達(dá)性解答了“從一個頂點v到達(dá)另一個頂點w,是否存在一條有向路徑”等類似問題。
深度優(yōu)先搜索
解答可達(dá)性問題,要借助深度優(yōu)先搜索算法。為了更好的理解深度優(yōu)先算法,先來搞清楚如何完全探索一個迷宮。
Tremaux搜索
完全探索一個迷宮的規(guī)則是:從起點出發(fā),不走重復(fù)路線,走到終點走出迷宮。具體流程:
- 每當(dāng)?shù)谝淮蔚竭_(dá)一個新的頂點或邊時,標(biāo)記上。
- 在走的過程中,遇到一個已標(biāo)記的頂點或邊時,退回到上一個頂點。
- 當(dāng)回退到的頂點已沒有可走的邊時繼續(xù)回退。
我想Tremaux搜索會給我們帶來一些啟發(fā),回到圖的深度優(yōu)先搜索算法。
package algorithms.graph;import algorithms.bag.Bag;
import ioutil.StdOut;/*** 基于深度優(yōu)先搜索(Depth First Search)解答有向圖頂點可達(dá)性問題。*/
public class DigraphDFS {private boolean[] marked;// 是否標(biāo)記過/*** 算法:在圖中找到從某個頂點出發(fā)的所有頂點** @param digraph* @param start*/public DigraphDFS(Digraph digraph, int start) {marked = new boolean[digraph.V()];// 初始化marked數(shù)組dfs(digraph, start);}/*** 算法:在圖中找到從某些頂點出發(fā)的所有頂點,這些頂點被作為一個集合傳入。** @param digraph* @param startSet*/public DigraphDFS(Digraph digraph, Iterable<Integer> startSet) {marked = new boolean[digraph.V()];for (int w : startSet) {dfs(digraph, w);}}/*** 查詢某個頂點是否被標(biāo)記(是否可達(dá),因為標(biāo)記過就是可達(dá)的)** @param v* @return*/public boolean marked(int v) {return marked[v];}/*** 深度優(yōu)先搜索核心算法,通過標(biāo)記,在圖中從v頂點出發(fā)找到有效路徑* <p>* 返回的是通過標(biāo)記形成的一條有效路徑。** @param digraph* @param v*/private void dfs(Digraph digraph, int v) {marked[v] = true;// 標(biāo)記起點可達(dá)。for (int w : digraph.adj(v)) {// 遍歷v頂點可達(dá)的一級頂點。if (!marked[w]) dfs(digraph, w);// 如果發(fā)現(xiàn)w頂點未到達(dá)過,則繼續(xù)從w開始dfs(即向前走了一步)}}public static void main(String[] args) {Digraph d = new Digraph(5);// 初始化五個頂點的圖d.addEdge(0, 1);d.addEdge(1, 0);d.addEdge(2, 3);d.addEdge(0, 4);Bag<Integer> startSet = new Bag<>();startSet.add(2);DigraphDFS reachable = new DigraphDFS(d, startSet);for (int v = 0; v < d.V(); v++) {if (reachable.marked(v)) {StdOut.print(v + " ");}StdOut.println();}/*** 輸出:*23*/}
}
startSet是入?yún)l件,只有一個值為2,即在圖中找尋2的有效路徑,通過圖中的邊我們可以看出,2的有效路徑只有3,所以輸出是正確的。
可達(dá)性的一種應(yīng)用:垃圾收集
我們都知道一般的對象垃圾收集都是計算它的引用數(shù)。在圖結(jié)構(gòu)中,把對象作為頂點,引用作為邊,當(dāng)一個對象在一段時間內(nèi)未被他人引用的時候,這個頂點就是孤立的,對于其他有效路徑上的頂點來說它就是不可達(dá)的,因此就不會被標(biāo)記,這時候,例如JVM就會清除掉這些對象釋放內(nèi)存,所以JVM也是一直在跑類似以上這種DFS的程序,不斷找到那些未被標(biāo)記的頂點,按照一定時間規(guī)則進(jìn)行清除。
有向無環(huán)圖
不包含有向環(huán)的有向圖就是有向無環(huán)圖,DAG,Directed Acyclic Graph。
上面我們循序漸進(jìn)的介紹了圖,有向圖,本節(jié)開始介紹有向無環(huán)圖,概念也已經(jīng)給出,可以看出有向無環(huán)圖是有向圖的一種特殊結(jié)構(gòu)。那么第一個問題就是
如何監(jiān)測有向圖中沒有有向環(huán),也就是如何確定一個DAG。
尋找有向環(huán)
基于上面的問題,我們要做一個尋找有向環(huán)的程序,這個程序還是依賴DFS深度優(yōu)先搜索算法,如果找不到,則說明這個有向圖是DAG。
棧
先來補個坑,其實前面包括背包我在之前都寫過,但因為前面那篇文章是我第一篇博文,我還太稚嫩,沒有掌握好的編輯器,也沒有粘貼代碼,所以這里有必要重新填坑。
package algorithms.stack;import ioutil.StdOut;import java.util.Iterator;
import java.util.NoSuchElementException;public class Stack<Item> implements Iterable<Item> {private int SIZE;private Node first;// 棧頂public Stack() {// 初始化成員變量SIZE = 0;first = null;}private class Node {private Item item;private Node next;}// 棧:往first位置插入新元素public void push(Item item) {Node temp = first;first = new Node();first.item = item;first.next = temp;SIZE++;}// 棧:從first位置取出新元素,滿足LIFO,后進(jìn)先出。public Item pop() {if (isEmpty()) throw new RuntimeException("Stack underflow");Item item = first.item;first = first.next;SIZE--;return item;}public boolean isEmpty() {return first == null;}public int size() {return this.SIZE;}@Overridepublic Iterator<Item> iterator() {return new Iterator<Item>() {Node node = first;@Overridepublic boolean hasNext() {return first != null;}@Overridepublic Item next() {if (!hasNext()) throw new NoSuchElementException();Item item = node.item;node = node.next;return item;}};}public static void main(String[] args){Stack<String> stack = new Stack<>();stack.push("heyheyhey");stack.push("howau");stack.push("231");StdOut.println(stack.SIZE);StdOut.println(stack.pop());}
}
我們要做尋找有向環(huán)的程序的話,要依賴棧的結(jié)構(gòu),所以上面把這個坑給填了,下面回歸到尋找有向環(huán)的程序。(當(dāng)然,你也可以直接使用java.util.Stack類)
package algorithms.graph;import ioutil.StdOut;import java.util.Stack;public class DirectedCycle {private boolean[] marked;// 以頂點為索引,值代表了該頂點是否標(biāo)記過(是否可達(dá))private Stack<Integer> cycle; // 用來存儲有向環(huán)頂點。// *****重點理解這里start****private int[] edgeTo;// edgeTo[0]=1代表頂點1->0, to 0的頂點為1。// *****重點理解這里end****private boolean[] onStack;// 頂點為索引,值為該頂點是否參與dfs遞歸,參與為truepublic DirectedCycle(Digraph digraph) {// 初始化成員變量marked = new boolean[digraph.V()];onStack = new boolean[digraph.V()];edgeTo = new int[digraph.V()];cycle = null;// 檢查是否有環(huán)for (int v = 0; v < digraph.V(); v++) {dfs(digraph, v);}}private void dfs(Digraph digraph, int v) {onStack[v] = true;// 遞歸開始,頂點上棧marked[v] = true;for (int w : digraph.adj(v)) {// 遍歷一條邊,v-> w// 終止條件:找到有向環(huán)if (hasCycle()) return;// 使用onStack標(biāo)志位來記錄有效路徑上的點,如果w在棧上,說明w在前面當(dāng)了出發(fā)點,if (!marked[w]) {edgeTo[w] = v;// to w的頂點為vdfs(digraph, w);} else if (onStack[w]) {// 如果指到了已標(biāo)記的頂點,且該頂點遞歸棧上。(棧上都是出發(fā)點,而找到了已標(biāo)記的頂點是終點,說明出發(fā)點和終點相同了。)cycle = new Stack<Integer>();for (int x = v; x != w; x = edgeTo[x]) {//起點在第一次循環(huán)中已經(jīng)push了,不要重復(fù)cycle.push(x);// 將由v出發(fā),w結(jié)束的環(huán)上中間的結(jié)點遍歷push到cycle中。}cycle.push(w);// push終點}}onStack[v] = false;// 當(dāng)遞歸開始結(jié)算退出時,頂點下棧。}public boolean hasCycle() {return cycle != null;}public Iterable<Integer> cycle() {return cycle;}public static void main(String[] args) {Digraph d = new Digraph(6);d.addEdge(0, 1);d.addEdge(1, 2);d.addEdge(2, 3);d.addEdge(3, 0);DirectedCycle directedCycle = new DirectedCycle(d);if (directedCycle.hasCycle()) {for (int a : directedCycle.cycle()) {StdOut.println(a);}} else {StdOut.println("DAG");}}
}
這段代碼不長但其中算法比較復(fù)雜,我盡力在注釋中做了詳細(xì)解釋,如有任何不明之處,歡迎隨時留言給我。
以上程序的測試用圖為
6 vertices, 4 edges
0: 1
1: 2
2: 3
3: 0
4:
5: 肉眼可以看出,這是一個0-1-2-3-0的一個有向環(huán),所以以上程序的執(zhí)行結(jié)果為:
3
2
1
0 先入棧的在后面,可以看出是0-1-2-3的有向環(huán)結(jié)構(gòu)。如果我們將圖的內(nèi)容改為:
6 vertices, 4 edges
0: 1
1: 2
2: 3
3:
4:
5: 0 則明顯最后一個拼圖3-0被我們打破了,變成了無所謂的5-0,這時該有向圖就不存在有向環(huán)。此時以上程序執(zhí)行結(jié)果為:
DAG DAG與BlockChain
上面一章節(jié)我們將DAG深挖了挖,我想到這里您已經(jīng)和我一樣對DAG的算法層面非常了解,那么它和如今沸沸揚揚的區(qū)塊鏈有什么關(guān)聯(lián)呢?本章節(jié)主要介紹這部分內(nèi)容。
在前面的文章中,我們已經(jīng)了解了區(qū)塊鏈技術(shù),無論是比特幣還是以太坊,都是基于一條鏈?zhǔn)浇Y(jié)構(gòu),實現(xiàn)了去中心化的,點對點的,trustless的一種新型技術(shù)。然而這條鏈?zhǔn)浇Y(jié)構(gòu)在面臨業(yè)務(wù)拓展的時候?qū)覍以馐苄碌奶魬?zhàn),例如塊存儲量問題,交易速度問題,數(shù)據(jù)總量過大,單節(jié)點存儲壓力等等。而DAG是基于圖的一種實現(xiàn)方式,之所以不允許有向環(huán)的出現(xiàn),是因為DAG可以保證結(jié)點交易的順序,可以通過上面介紹過的有效路徑來找到那根主鏈。如果出現(xiàn)了有向環(huán),那系統(tǒng)就亂了。如果沒有有向環(huán)的話,DAG中可以有多條有效路徑連接各個頂點,因此DAG可以說是更加完善,強大的新一代區(qū)塊鏈結(jié)構(gòu)。
轉(zhuǎn)存失敗重新上傳取消
目前非常有名的采用DAG技術(shù)的區(qū)塊鏈產(chǎn)品有DagCoin,IOTA,ByteBall等,他們都是基于DAG,在性能和儲量上面有了全面的提升。
這里面仍然會有“分叉”的可能,處理方式也是相同的,看哪個結(jié)點能夠有新的后續(xù),這個部分我們在講“叔塊”的時候說過。
區(qū)塊鏈采用DAG結(jié)構(gòu)以后稱為了blockless,無塊化的結(jié)構(gòu),即我們不再將交易打包到塊中,以塊為單元進(jìn)行存儲,而是直接將交易本身作為基本單元進(jìn)行存儲。另外,DAG也有雙花的可能,也是上面“分叉問題”引起的,但它在確認(rèn)有效路徑以后會自動恢復(fù)。同時,DAG是異步共識,具體機制還不了解,但它解決了交易性能問題。
總結(jié)
本文循序漸進(jìn)地從圖到有向圖到有向無環(huán)圖,詳細(xì)地介紹了相關(guān)術(shù)語,api代碼實現(xiàn),也補充入了背包和棧的代碼實現(xiàn),重點研究了圖的深度優(yōu)先搜索算法以及尋找有向環(huán)算法。最后對DAG和區(qū)塊鏈的關(guān)系進(jìn)行了簡介,希望隨著技術(shù)發(fā)展,DAG有望成為真正的區(qū)塊鏈3.0。
參考資料
Algorithms 4th,網(wǎng)上資料
更多文章請轉(zhuǎn)到醒者呆的博客園。
總結(jié)
以上是生活随笔為你收集整理的算法精解:DAG有向无环图的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python xrange() 函数
- 下一篇: 【图论】有向无环图的拓扑排序