当我们学习数据结构的时候,总是觉得很枯燥,而当我们解决实际问题的时候,又往往因为对数据结构了解的匮乏而束手无策。从问题中来,到问题中去,在某一点上的深入思考并且不断的实践积累,或许是个笨办法,但笨办法总是比没办法好一些。本文是老码农对DAG的随手笔记,积累成文。

什么是DAG?

DAG,Directed Acyclic Graph即「有向无环图」。

从计算机的视角来看,DAG 是一个图,图与数组、队列、链表等一样,都是是一种数据结构。图是由顶点和连接顶点的边构成的数据结构,在计算机科学中,图是最灵活的数据结构之一,很多问题都可以使用图模型进行建模求解。例如,人与人之间的社交网络、分析计算机网络的拓扑结构已确定两台计算机是否可以通信、物流系统中找到两个地点之间的最短路径等等。

回顾一下图的相关概念:

  • 顶点:图中的一个点

  • 边:连接两个顶点的线段

  • 相邻:一个边的两头顶点成为相邻

  • 度数:由一个顶点出发,有几条边就称该顶点有几度

  • 路径:通过边来连接,按顺序的从一个顶点到另一个顶点中间经过的顶点集合

  • 简单路径:没有重复顶点的路径

  • 环:至少含有一条边,并且起点和终点都是同一个顶点的路径

  • 简单环:不含有重复顶点和边的环

  • 无环图:是一种不包含环的图

  • 连通图:如果一个图中,从任意顶点均存在一条路径可以到达另一个任意顶点,该图称为连通图

  • 稀疏图:图中每个顶点的度数都不较小

  • 稠密图:图中每个顶点的度数都较大

如果图中的边可以是有方向的,边的方向性表明了两个顶点直接的不同不关系。例如,地图应用中必须存储单行道的信息,避免给出错误的方向。如果图中任意两个顶点之间的边都是有向边,这个图就是有向图。如果有一个非有向无环图,且A点出发向B经C可回到A,形成一个环。将从C到A的边方向改为从A到C,则变成有向无环图,即DAG。

按照数学上的定义,DAG是一个没有有向循环的、有限的有向图。具体来说,它由有限个顶点和有向边组成,每条有向边都从一个顶点指向另一个顶点;从任意一个顶点出发都不能通过这些有向边回到原来的顶点。也就是说,它由 顶点 Vertex 和 边 Edge (也称为弧)组成,每条边都从一个顶点指向另一个顶点,沿着这些顶点的方向 不会形成一个闭合的环 。DAG与代数拓扑学中的偏序集(Partially Ordered Set,Poset)有紧密联系。偏序关系相同的任意两个图会有相同的拓扑排序集。事实上,任何一个DAG都唯一对应一个Poset, 而所有的Poset都是DAG,所以它们在本质上是一种事物。

DAG具有一些很好性质,比如很多动态规划的问题都可以转化成DAG中的最长路径、最短路径或者路径计数的问题。

DAG的特性

DAG 具有空间结构和时间序列的混合特性,与数据结构中的树密切相关,其拓扑排序和最短路径的计算,都有着独到的特点。

DAG 与树的关系

DAG是树的泛化,也是ploy tree的泛化。tree 是层次化,按照类别或者特性可以细分到原子单元,树其实就是一种无环连通图。DAG 从源开始细分,但是中间可以有合,有汇总。D就是可以合的点。

因为有向图中一个点经过两种路线到达另一个点未必形成环,因此有向无环图未必能转化成树,但任何有向树均为有向无环图。

DAG 的拓扑排序

拓扑排序是一个可以把所有的顶点排序的算法,它排序的依据是深度优先搜索算法的完成时间。可以根据拓扑排序来计算有向无环图(的单源最短路径),因为拓扑排序正好是建立在无环的基础上,在这个图中没有负权重边以及回路边。

拓扑排序是将图中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

对于一个DAG,可以这样确定一个图中顶点的顺序:对于所有的u、v,若存在有向路径u-->v,则在最后的顶点排序中u就位于v之前。这样确定的顺序就是一个DAG的拓扑排序。拓扑排序的特点如下:(1)所有可以到达顶点v的顶点u都位于顶点v之前;(2)所有从顶点v可以到达的顶点u都位于顶点v之后。

另外,只有有向无环图才存在拓扑排序,一个图的拓扑顺序不唯一。

实现拓扑排序主要有两种方法:入度表和DFS。在DFS实现拓扑排序时,用栈来保存拓扑排序的顶点序列;并且保证在某顶点入栈前,其所有邻接点已入栈。下面列出的是用C语言实现的入度表拓扑排序示例代码:

#define MAX_NODE 1000
#define MAX_EDGE_NUM 100000
struct Edge{int to;int w;int next;
};
Edge gEdges[MAX_EDGE_NUM];
int gHead[MAX_NODE];
bool gVisited[MAX_NODE];
int gInDegree[MAX_NODE];
int gEdgeCount;
void InsertEdge(int u,int v,int w){int e = gEdgeCount++;gEdges[e].to = v;gEdges[e].w = w;gEdges[e].next = gHead[u];gHead[u] = e;gInDegree[v] ++;   //入度加1
}
void TopoSort(int n/*节点数目*/){queue<int> zero_indegree_queue;for (int i = 0; i < n; i++){if (gInDegree[i] == 0)zero_indegree_queue.push(i);}memset(gVisited,false,sizeof(gVisited));while (!zero_indegree_queue.empty()){int u = zero_indegree_queue.front();zero_indegree_queue.pop();gVisited[u] =true;//输出ufor (int e = gHead[u]; e != -1; e = gEdges[e].next){int v = gEdges[e].to;gInDegree[v] --;if (gInDegree[v] == 0){zero_indegree_queue.push(v);}}}for (int i = 0; i < n; i++){if (!gVisited[i]){//环!无法形成拓扑序}}
}

DAG 的单源最短路径

图中节点的单源最短路径可以使用Dijkstra,BellmanFord, SPFA算法,而对于有向无环图DAG来说,可以通过简单的动态规划来进行求解。DAG的独特之处是所有节点可以线性化(拓扑序列),使得所有边保持从左到右的方向。

给定一个DAG和一个源点,可以得到该源点到其他所有的顶点的最短路径。如果是无负权,可以用djistra算法完成。但如果存在负权,则不行。同时,djistra算法效率并不高,既然是DAG,则可以利用拓扑排序的结果求出给定源点的最短路径,其时间复杂度是线性时间复杂度O(V+E)。

DAG 的单源最短路径算法原理如下:

1) 初始化dist[] = {INF, INF, ….} ,dist[s] = 0 是单源顶点

2)创建所有定点的拓扑排序

3) 对拓扑排序中的每个顶点u 做如下处理,即处理u 的每个相邻顶点:if (dist[v] > dist[u] + weight(u, v)) dist[v] = dist[u] + weight(u, v) 

具体地, 用Python 实现的DAG最短路径算法代码示例如下:

# Python program to find single source shortest paths
# for Directed Acyclic Graphs Complexity :OV(V+E)
from collections import defaultdict# Graph is represented using adjacency list. Every
# node of adjacency list contains vertex number of
# the vertex to which edge connects. It also contains
# weight of the edge
class Graph:def __init__(self,vertices):self.V = vertices # No. of vertices# dictionary containing adjacency Listself.graph = defaultdict(list)# function to add an edge to graphdef addEdge(self,u,v,w):self.graph[u].append((v,w))# A recursive function used by shortestPathdef topologicalSortUtil(self,v,visited,stack):# Mark the current node as visited.visited[v] = True# Recur for all the vertices adjacent to this vertexif v in self.graph.keys():for node,weight in self.graph[v]:if visited[node] == False:self.topologicalSortUtil(node,visited,stack)# Push current vertex to stack which stores topological sortstack.append(v)''' The function to find shortest paths from given vertex.It uses recursive topologicalSortUtil() to get topologicalsorting of given graph.'''def shortestPath(self, s):# Mark all the vertices as not visitedvisited = [False]*self.Vstack =[]# Call the recursive helper function to store Topological# Sort starting from source verticefor i in range(self.V):if visited[i] == False:self.topologicalSortUtil(s,visited,stack)# Initialize distances to all vertices as infinite and# distance to source as 0dist = [float("Inf")] * (self.V)dist[s] = 0# Process vertices in topological orderwhile stack:# Get the next vertex from topological orderi = stack.pop()# Update distances of all adjacent verticesfor node,weight in self.graph[i]:if dist[node] > dist[i] + weight:dist[node] = dist[i] + weight# Print the calculated shortest distancesfor i in range(self.V):print ("%d" %dist[i]) if dist[i] != float("Inf") else  "Inf" ,g = Graph(6)
g.addEdge(0, 1, 5)
g.addEdge(0, 2, 3)
g.addEdge(1, 3, 6)
g.addEdge(1, 2, 2)
g.addEdge(2, 4, 4)
g.addEdge(2, 5, 2)
g.addEdge(2, 3, 7)
g.addEdge(3, 4, -1)
g.addEdge(4, 5, -2)# source = 1
s = 1print ("Following are shortest distances from source %d " % s)
g.shortestPath(s)# This code is contributed by Neelam Yadav

DAG的应用

鉴于DAG的一般性,具有广泛的应用场景。

DAG 的存储结构用例

作为数据结构,DAG 在数据存储方面非常著名的使用场景就是Git。Git采用了Merkle Tree+ DAG作为一个组合的数据结构Merkle DAG,来实现了分布式的的版本控制。

IPFS 参考了Git的实现思想,同样使用了Merkle DAG 作为核心的数据结构,即后来称为IPLD, 在 IPFS 生态系统的“蜂腰”模型中处于腰的位置,如下图所示:

Merkle Tree通常也被称作Hash Tree,顾名思义,就是存储hash值的一棵树。在Merkle Tree 的基础上,Merkle DAG是一个有向无环图,可以简单的理解成一棵树,且没有Merkle Tree那样严格的限制(例如平衡树),其特点是:

  • 父节点的哈希值由子节点的哈希值决定,即父节点的哈希值是由子节点的哈希值拼接得来的字符串哈希而成。

  • 父节点中包含指向子节点的信息。

Merkle DAG保留了Merkle Tree的精华部分,即任何一个下层节点改动,都将导致上层节点的哈希值变动,最终的根节点的哈希值也变动。在IPFS网络中,存储文件时,首先会将文件切片,切割成256KB大小的文件。之后循环调用(MerkleDAG.Add)方法构建文件MerkleDAG。文件hash值创建流程如下:

  1. 将切片之后的文件进行sha-256运算

  2. 将运算结果选取0~31位

  3. 将选取结果根据base58编码,运算结果前追加Qm 即为最后结果作为文件的46位hash值。

在IPFS中,使用DAGService维护MerkleDAG,为MerkleDAG提供删除和增加的权限。其中的Merkle DAG为多叉树结构,最多为174叉树。

通过Merkle DAG, IPFS能够轻松确保和验证P2P格式的计算机之间共享数据的完整性,这使得它们对系统非常有用。

DAG 的因果关系用例

以探讨影响因素或者控制偏倚为目的回归模型,要求自变量和因变量往往存在着因果关系,所以自变量筛选首先需要考虑自变量能否纳入到模型,严格挑选自变量进入模型。

DAG可以说是回归分析的灵魂所在,是最高指导方针。基于DAG构建因果关系网络,从而找到合适进入模型的自变量。箭头发出对象为因,箭头指向为果。所有变量因果关系通过方向线形成的单向网络,即DAG。

例如,贝叶斯网络是表示多个概率事件的关联网络。顶点表示事件,后续事件的发生可能性则可以通过其在有向无环图的前驱节点的发生概率计算出来。

动态规划的DAG 实现

什么是动态规划呢?问题可以分解成若干相互联系的阶段,在每一个阶段都要做出决策,全部过程的决策是一个决策序列。要使整个活动的总体效果达到最优的问题,称为多阶段决策问题。动态规划就是解决多阶段决策最优化问题的一种思想方法。

将所给问题的过程,按时间或空间特征分解成若干相互联系的阶段,以便按次序去求每阶段的解。各阶段开始时的客观条件叫做状态。当各段的状态取定以后,就可以做出不同的决定,从而确定下一阶段的状态,这种决定称为决策。具体地,动态规划的递推需要一个线性或者树形的顺序,对于DAG,可以将节点按照拓扑顺序来进行线性化。具体地,对于当前的节点v,在拓扑序列中向前查找,可能找到一些可以到达该顶点的其他节点,然后利用 dist[v] = min{dist[u] + w[u][v] | u 可以到达v}来进行动态规划的递推。

基于DAG 调度用例

在有相互依赖的调度系统中,DAG 有着非常典型的应用。这里以Spark 为例进行说明。

在Spark中的每一个操作生成一个RDD,RDD之间形成一条边,最后这些RDD和他们之间的边组成一个有向无环图,这个就是DAG。

Spark中的RDD依赖关系分为两种:窄依赖(Narrow Dependencies)与宽依赖(Wide Dependencies,源码中称为Shuffle Dependencies)会根据宽依赖窄依赖来划分具体的Stage,而依赖有2个作用:

  • 用来解决数据容错的高效性;

  • 其二用来划分stage。

原始的RDD通过一系列的转换就形成了DAG,有了可计算的DAG,Spark内核下一步的任务就是根据DAG图将计算划分成任务集,也就是Stage,这样可以将任务提交到计算节点进行真正的计算。Spark计算的中间结果默认是保存在内存中的,Spark在划分Stage的时候会充分考虑在分布式计算中可流水线计算的部分来提高计算的效率,而在这个过程中Spark根据RDD之间依赖关系的不同将DAG划分成不同的Stage。对于窄依赖,partition的转换处理在一个Stage中完成计算。对于宽依赖,由于有Shuffle的存在,只能在parent RDD处理完成后,才能开始接下来的计算,因此宽依赖是划分Stage的依据。

Spark 执行时的处理流程如下:

1)用户代码定义RDD的有向无环图

RDD上的操作会创建新的RDD,并引用它们的父节点,这样就创建了一个图。

2)行动操作把有向无环图强制转译为执行计划

当调用RDD的一个行动操作时,这个RDD就必须被计算出来。这也要求计算出该RDD的父节点。Spark调度器提交一个作业来计算出所有必要的RDD。这个作业会包含一个或多个步骤,每个步骤其实也就是一波并行执行的计算任务。一个步骤对应有向五环图中的一个或多个RDD,一个步骤对应多个RDD是因为发生了流水线执行。

3)任务于集群中调度并执行

步骤是按顺序处理的,任务则独立的启动来计算出RDD的一部分。一旦作业的最后一个步骤结束,一个行动操作也就执行完了。

DAG 的区块链用例

最早在区块链中引入DAG概念作为共识算法是在2013年,bitcointalik.org由ID为avivz78的以色列希伯来大学学者提出,也就是GHOST协议,作为比特币的交易处理能力扩容解决方案;Vitalik在以太坊紫皮书描述的POS共识协议Casper,也是基于GHOST POW协议的POS变种。

后来,有人提出用DAG的拓扑结构来存储区块,解决区块链的效率问题。区块链只有一条单链,打包出块无法并发执行。如果改变区块的链式存储结构,变成DAG的网状拓扑就可以实现并发写入。在区块打包时间不变的情况下,网络中可以并行打包N个区块,网络中的交易效率就提升了N倍。

那个时候,DAG跟区块链的结合依旧停留在类似侧链的解决思路,交易打包可以并行在不同的分支链条进行,达到提升性能的目的。此时,DAG还是区块的概念。

2015年9月,Sergio Demian Lerner发表了 《DagCoin: a cryptocurrency without blocks》一文,提出了DAG-Chain的概念,首次把DAG网络从区块打包这样粗粒度提升到了基于交易层面。DagCoin的思路,让每一笔交易都直接参与维护全网的交易顺序。交易发起后,直接广播全网,这样省去了打包交易出块的时间。也就是说,交易发起后直接广播网络确认,理论上效率得到了质的飞跃。

进一步,DAG演变成了完全抛弃区块链的一种解决方案。2016年7月,基于IOTA横空出世,随后ByteBall也登场了,IOTA和Byteball是头一次DAG网络的真正技术实现,此时,号称无块之链、独树一帜的DAG链家族雏形基本形成。

从某种程度上说,DAG可能是面向未来的新一代区块链,从单链进化到树状和网状、从区块粒度细化到交易粒度、从单点跃迁到并发写入,这是区块链从容量到速度的一次革新。

一句话小结

有向无环图有许多科学的和计算的应用,从生物学到社会学再到我们所熟悉的计算机领域,而且,DAG的原理并不复杂,关键在于把目标问题抽象为DAG,进而使用DAG的相关特性来解决问题。

【参考资料与关联阅读】

  • https://www.lwlwq.com/post-dag.html

  • https://zhuanlan.zhihu.com/p/275992858

  • https://www.jianshu.com/p/84cdab3030cb

  • https://en.wikipedia.org/wiki/Directedacyclicgraph

  • https://www.cnblogs.com/gtarcoder/p/4900516.html

  • https://www.geeksforgeeks.org/shortest-path-for-directed-acyclic-graphs/

  • ES的一知半解

  • NFT 的一知半解

  • 温故知新,HTTP/2

  • 服务可用性的一知半解

  • 对AI产品经理的一知半解

  • 人工智能伦理学的一知半解

有向无环图(DAG)的温故知新相关推荐

  1. 后端 绘制有向无环图(DAG图)

    因为公司的某个系统比较古老,里面的 job 的管理几乎都是直接通过操作数据库来实现的,对于一般的操作还可以忍受,但是每次想知道 job 之间的依赖关系的时候,就会相当难受,于是便脑袋很抽地一定要写一个 ...

  2. 数据驱动应用(三):异构数据服务(基于有向无环图DAG)

    概述 主要概念 数据服务(Data Service):对异构数据源,基于有向无环图,提供异构数据的查询和推送能力. 指标:用于衡量事物发展程度的单位或方法,它还有个IT上常用的名字,也就是度量.例如: ...

  3. Apache Spark中的有向无环图DAG

    Apache Spark中的有向无环图DAG 由DATAFLAIR TEAM ·更新· 2018年11月21日 1.目的 在本Apache Spark教程中,我们将了解Apache Spark中的DA ...

  4. 大数据工作流任务调度--有向无环图(DAG)之拓扑排序

    拓扑排序(Topological Sorting) 回顾基础知识: 1.图的遍历: 图的遍历是指从图中的某一个顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问一次且仅访问一次.注意树是一种特 ...

  5. C#实现有向无环图(DAG)拓扑排序

    对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在 ...

  6. 有向无环图DAG 拓扑排序 代码解释

    目录: DAG定义 举例描述 实际运用 算法描述 算法实战 算法可视化 定义 在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序(英语:Topological ...

  7. `Computer-Algorithm` 有向无环图DAG

    Contents 有向无环图 定义 性质 应用 最小路径点覆盖 最小路径重复点覆盖 路径独立集 至少新添加几条边,使得DAG成为SCC 有向无环图 定义 性质 + let HHH be the set ...

  8. 有向无环图(DAG)拓扑排序的两种方法

    如下图的DAG: 第一种: (1)从AOV网中选择一个没有前驱的顶点并且输出它: (2)从AOV网中删除该顶点,并且上去所有该顶点为尾的弧: (3)重复上述两步,直到全部顶点都被输出,或者AOV网中不 ...

  9. java 有向无环图 树_拓扑排序-有向无环图(DAG, Directed Acyclic Graph)

    条件: 1.每个顶点出现且只出现一次. 2.若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面. 有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说. 一 ...

最新文章

  1. Linux虚拟机示范
  2. 内部收益率irr_国寿鑫耀东方年金险,内部收益率IRR测算
  3. C#比较dynamic和Dictionary性能
  4. Yet another nio framework for java
  5. adduser useradd userdel /etc/password【原创】
  6. linux 临时 目录权限,linux文件和目录权限的设置
  7. 基础:Linux 核心源码是怎么组织的?-转
  8. shell脚本基础练习题
  9. web通用组件+Axure原型+Axure元件库+Axure后台管理系统框架模板+大屏数据可视化元件库+智慧社区管理系统大屏+图表组件+表单组合+智慧数据看板+通用大屏图表原件库+电脑端常用组件
  10. 轻松解决加密的PDF如何编辑简单技巧
  11. 鉴源实验室丨汽车网络安全需求分析方法综述
  12. 基本农田卫星地图查询_天地图山东,购房者勘测利器,国产骄傲
  13. SELECT 1 FROM DUAL中的DUAL的作用
  14. python量化交易第三章
  15. table 点击文字按钮预览图片
  16. HTML5 中 40 个最重要的技术点
  17. 如何修改ICO文件的尺寸
  18. 【工具】goland pull代码 出现 you local changes would be overwritten by merge.Commit, stash or revert ......
  19. 公众号榜单 | 2020·7月公众号原创排行榜重磅发布
  20. JavaScript生成uuid的四种方法

热门文章

  1. PHP模板引擎的原理与实践
  2. 黑马程序员云道页面—巩固html和css(新特性没有使用)
  3. Dubbo面试题及答案,2021年Dubbo面试题-持续更新中
  4. 人月神教α阶段冲刺报告(11/12)
  5. 交换机中的交换表的自学习
  6. 线性表之单链表基本操作
  7. 尚硅谷JavaWeb笔记——HTML、CSS(后端补充前端知识,这些就够了)
  8. sh(shell脚本)--学习笔记
  9. css实现横向滚动条
  10. HTML5+CSS3基础(day6)