图&图应用

1.图类型&顶点类型&边类型&邻接矩阵(Graghmatrix)(这里图实现的是带权有向图,每个顶点储存有数据,每条边也储存有数据)

图类型(Gragh):一堆接口用于继承(这个和词典一样,只是一个空空的接口,而且很多接口都是纯虚函数需要重写,所以只是事先约定好这种类型有什么功能,具体实现交给它派生的类型)

成员变量只有点数n以及边数e。

顶点类型(Vertex):成员变量有Tv模板类型的数据data,出度、入度、状态(枚举数据类型:undiscovered、discovered以及visited)、时间标签(dtime、ftime),遍历树中的父节点(rank无符号整型,priority遍历树中的优先级。(即数据加上一堆属性,和二叉树节点比,没了父子的节点位置)

边类型(Edge):成员变量有Te模板类型的data,权重weight以及状态(枚举数据类型:UNDETERMINED, TREE, CROSS, FORWARD, BACKWARD

Graghmatrix:这才是具体实现Graph的类,类似于词典中的hashtable。

首先Graghmatrix继承了Gragh类,其中有两个成员变量(没加上Gragh的n和e),一个是Vector<Vertex<Tv>> V(顶点类型向量)、Vector<Vector<Edge<Te>*>> E(里面空间中最重要的是一个二维数组)。注意大写的E是向量,小写的e是边数量。

接口方面:分为对顶点和边。

对顶点的基本操作有:vertex()(注意这是小写的vertex,大写的Vertex是顶点类型),该接口返回的是指定位置(循秩访问)的顶点的data,类似还有inDegree()、outDegree,查询顶点的入度和出度;以及一系列查询顶点属性的函数如status()即返回相应位置顶点的status。其中有意思的是nextNbr()接口,可以返回一个顶点指向的秩最大的顶点。

对顶点的动态操作:插入顶点insert(),对于向量V直接插入即可,对于向量E(邻接矩阵,表示边),首先对每个E[](储存边类型地址的向量)在末尾插入空地址,n++,之后在E(储存向量的向量)末尾一个插入一个新向量,新向量容量和规模都是n,且值都为null。

删除顶点remove():首先删除以该顶点为起点的边,再删除E[],再删除点,n--,再删除以该顶点为终点的。(注意在顶点的删除和插入后依然保持E向量的规模等于E[]向量的规模)。

对边的基本操作:查询操作与点的查村操作类似,函数名即是边的属性值,而且edge返回的是边的data(注意是小写),函数参数有两个(点只有一个)。

对边的动态操作:注意函数名与点的插入、删除一样,因为发生了函数重载。

用这种方式Graghmatrix(邻接矩阵)实现的gragh,有一个缺点是空间上可能造成较大的浪费,因为边的数量可能远远小于极限值n^2。ALgragh(邻接表)是图的另一种实现方式,主要思想是用arcnode表示边,每个arcnode除了数据外还包括下个arcnode的地址以及关联点(终点)的地址,普通的Vnode除了数据外还包括一个arcnode地址,即由这个可以实现一串arcnode列表,用来表示以该顶点为起点的所有边,这样的空间复杂度控制在O(n+e),对于边少的图(稀疏图如平面图e与n成线性关系)可以降低空间复杂度。

2. 图类型中不是纯虚函数的接口

图中的纯虚函数(点、边的查询和插入删除等)是在不同的继承中有不同的实现(邻接矩阵和邻接表),除此之外还有不是纯虚函数的接口(不需要重写,即可以利用以上的纯虚函数接口实现的)。如reset(),将所有顶点的属性值复位。

BFS(Breadth-first-search)广度优先搜索:单个连通域的BFS()(即从某点出发,把能达到的点和边都搜索一 遍),该函数主要是将点的状态和边的状态属性以及其他属性做调整,将点的状态由undiscovered变成discovered,再变成visited(当然想要访问该节点数据只需加代码即可),将边的状态由undetermined变成tree或者是cross。如果图只有单个连通域,那么经过一轮BFS,可以完全覆盖所有的点和边,所有的点和状态为tree的边构成了图的一棵支撑树。有多个连通域可以bst()循环调用BST完成搜索。

BFS算法用邻接表的方式实现,可以达到O(n+e)的复杂度。用邻接矩阵的话,因为查找一个点的邻居需要O(n)复杂度,所以可能需要O(n^2).

DFS (depth first search)深度优先搜索:最后也能达成将起点能到达的点和边都遍历一遍(与BST一样),如果是单连通的图,那么可以生成一棵DFS的支撑树,与BFS支撑树不一样的是,在BFS中,顶点变为visited的顺序是层次遍历,而在DFS中是后序遍历(discovered的顺序是先序遍历)。都可以通过小写的dfs()接口,对整个图进行搜索,生成DFS森林(C棵树,n-c条树边)与BFS一致。(注意dfs的开始的顶点不同会产生不同结果,考虑一个链状有向图,如果开始的是起点,那就一棵树,但如果不是起点,可能产生最多达到n棵树,每棵树都只有一个节点)同样用连接表的方式可以实现O(n+e)的时间复杂度。

注意BFS与DFS中的顶点属性的dtime和ftime。对于BFS,顶点discovered和visited的顺序一致,dtime表示代数(第几代),ftime表示每一代中的顺序,总的顺序是先比较代数,再比较每一代的顺序。对于DFS,discovered的顺序是先序遍历,visited是后序遍历,所以祖先的dtime要小于孩子,ftime要大于孩子。即括号引理(parenthesis lemma),如果一个节点的(dtime,ftime)是另一个节点的子集,那么在DFS树中该点是另一个点的后代。

拓扑排序:

概念:考虑这样一个现象,学习课程有先修课(如果A是B的先修课,那用图表示就是A指向B,每个节点表示课,边表示是否有先修关系),那么一系列课程以及其先修关系可以用图表示。拓扑排序的对象是针对这些课程(即图中的节点),需要满足的要求是,每个课程的先修课程一定要在排在该课程之前(每个节点的先驱节点要排在节点之前)。注意如果图中有环,那么将无法用线性的方式对构成环的节点排序,所以只有DAG(directed acyclic gragh)有向无环图才存在拓扑排序。(而且有向无环图一定可以进行拓扑排序)

要实现拓扑排序,可以有两种思路,一种是不断将0入度的顶点排序(排完将其删除,然后将删除后入度为0的点加入待排序的栈),如果最后没有删除干净,说明不是DAG,如果是输出队列即可。另一种思路,将0出度的顶点压入栈然后逆序输出,每压入栈就等效于删除了该点,再将0出度的压入栈。其实这种用DFS(更准确是dfs全图)即可,一旦visited即压入栈,如果有backward边说明不是DAG。Stack<Tv>* tSort( Rank ); //基于DFS的拓扑排序算法,tsort针对全图,rank值取什么值对结果没影响。bool TSort( Rank, Rank&, Stack<Tv>* ); //(连通域)基于DFS的拓扑排序算法。

 

双连通分量分解(BCC Bi-Connected Components极大的双连通子图):注意这是针对无向图的。

对于识别无向图中的关节点(删除会引起连通量变化的点),可以做一次dfs,对dfs中的点分类进行判断。叶子节点:不可能是关节点。(因为除了树边外,叶子节点其他边都是backward边,删除它不会影响其他点的连通性),根节点:非关节点的根节点一定是只有一棵子树,而关节点的根节点有至少两棵子树,所以可以根据根节点子树数量判断是否是关节点。内部节点:该节点如果存在一个子树的所有后代都没有连接到该节点的真祖先,那么该节点为关节点;反之,如果所有子树都存在至少一个后代连接到了真祖先,那么其为非关节点。

在具体实现中,为每个节点引入hca(可以连接到的最高的祖先)属性值,可以将ftime属性定义为hca(因为在算法中ftime用不上),hca用dtime来表示,dtime越小说明越高,即hca越小越高。只要在每次弹出时连着当时的在调用函数BCC的顶点,即构成一组BCC。void BCC( Rank, Rank&, Stack<Rank>& ); //(连通域)基于DFS的双连通分量分解算法 void bcc( Rank ); //基于DFS的双连通分量分解算法

优先级搜索:template <typename PU> void pfs( Rank, PU ); //优先级搜索框架 template <typename PU> void PFS( Rank, PU ); //(连通域)优先级搜索框架,主要是对PU优先级更新器,通过定义具体的优先级更新策略prioUpdater,即可实现不同的算法功能。具体例子可以看下面的dijkstra算法的实现。

最短路径:void dijkstra( Rank ); //最短路径Dijkstra算法

把一个无向带权图可以制作成一个物理模型:节点用小球表示,带权的边用长度等于权的绳子连接,然后用一个带磁力的黑板能吸住小球,将源球放在黑板最上端。不断上移源球。随着源球的上升(过程忽略水平距离,而且一开始所有球都处于黑板最上端,即所有球在同一水平线,下图只是为了表达清晰各个球之间的距离),首先脱离黑板的应该是A球,在s球上升2之后,接下来脱离黑板的球应该是min(到s球距离,到A球距离+2)最小的那个球,也就是再上升一个高度,s球高度达到3,B球离开。注意此时AB之间的绳子是松的,长度为2,但是AB距离为1,而且一直是松的。接下来的判断与此类似。这样将所有球都拉出,每个球的最短路径长度就是距离s球的距离,最短路径就是拉直的绳子。为什么这种拉绳子的结果是正确的呢?首先只要是有路径到达的点,一定存在最短路径,而且路径上点一定也是最短路径,所以,从一开始就迭代的方式是对的。注意将所有最短路径取并(即将所有拉伸的绳子),是一个无环连通的树,因此通过PFS框架做过dijkstra算法后,相应的边变为树边,树上所有节点的路径即使最短路径。

 实现:用PFS的框架可以完美实现,只需写一个DijkPU的PU(优先级更新器)。因为PFS框架是visited后更新优先级,再选择优先级高的visited,按此循环;而Dijk算法正好如此,一个上岸后更新优先级(即到s点距离),选择距离短的(优先级高的),然后再更新优先级。

最小支撑树,Prim算法:

概念:支撑树覆盖图的所有节点,而且是一棵树,所以连通无环,树边比节点数小1,所以具有相同数量树边的支撑树可能不止一个,最小支撑树的最小包含了权重,所以一般只有一棵。

MST(minimal spanning tree)是最小支撑树,SPT是shortest path tree最短路径树,两者不一样。

Prim算法的思路是从一个点出发,加入树集合,之后更新邻居的优先级(prim的优先级与dijk的区别是,prim更新优先级是对新加入树集合点的邻居的距离与之前优先级的较小值,而dijk是新加入树集合点的邻居的距离与新加入树集合点优先级之和与之前优先级的较小值)然后选取优先级高的点加入树集合,如此迭代。可以注意到每次加入新的树集合的点对优先级更新都是新加入点的邻居,所以这个算法和dijk算法一样,完全符合PFS的框架,只需要写PU即可。

对于这算法的正确性,感觉要有严格的证明?

最小支撑树,Kruskal算法:

思路是迭代选取最小权的边,由n棵只有一个顶点的森林,变成一棵有n个顶点的树。这也是一种贪心算法,每次操作都是局部最优。(是否想起了huffman编码,由森林变成树)。正确性依然要严格证明。

实现:这里引入了union-find数据结构(并查集)。其中有两个成员变量,一个数组int [] parent,一个int n,四个接口:find,union,connected,count。在逻辑上是一开始有n棵树,每棵树只有一个节点,union的功能是将两棵树合成一棵树,find的功能是返回当前节点的根节点,connect是查询两个节点是否在同一棵树,count是返回当前一共有多少棵树。为什么这么多棵树(森林)可以用一个数组实现呢,因为这些树在合并过程中的总边数并没有改变,一直是n(注意根节点有一条指向自身的边),所以只要有个数组储存每个节点的父亲即可(这也是为什么数组名字叫parent)。所以一开始初始化,n节点的parent[n]就等于n,表明这n个都是根节点。find操作输入一个n,要返回n所在树的根节点,只需不断的比较n于parent[n],如果不相等,就将n取做parent继续向上迭代,直到n=parent[n],即到达根节点;对于union,将两棵树合并,只需将一棵树的根节点作为另一棵树的孩子即可(该树根节点之前的parent是自身),connect只需find两个参数,如果根节点相同即在同一棵树;count返回一个参数n即可(该参数在每次union成功时都减1,因此可以表示树的棵树)。考虑复杂度,主要的时间复杂度集中在find上,即树的高度。为解决此问题,可以保证每次都是小树接到大树上,这样能使得树平衡一些。要实现这种想法只需要在定义一个size数组,每次union的过程中记录树的规模(注意树是用根节点标识的,所以规模也是由根节点标识,一棵没用的根节点的规模忽略即可)。在find的过程中还可以对树进行折叠,进一步对树高进行控制。(想到了伸展树,每一次查找将查找点向上移动的同时双层伸展,这个也是查找,也要向上移动,这个还没有二叉搜索树的限制,可以轻易改变树的结构)因为要频繁进行find,所以树高能控制在常数水平(直觉)。Union-Find 算法的复杂度可以这样分析:构造函数初始化数据结构需要 O(N) 的时间和空间复杂度;连通两个节点union、判断两个节点的连通性connected、计算连通分量count所需的时间复杂度均为 O(1)。

利用unionfind如何实现kruskal,首先创建UF类(union find数据结构),UF(n)进行初始化,根据最短边进行union,最后n=1时(连通度为1,只剩一棵树),UF中的树即是kruskal算法得出的最小支撑树。

Floyd-Warshall算法:

d k(u,v)可以递归计算,d n(u,v)即是u与v之间的最短距离,蛮力递归需要极长时间(指数级),原因是存在很多重复的递归实例。(想起了fibnacci数的计算,蛮力递归也是很多重复的递归调用,解决办法是重头开始,用表记录每个fibnacci数)

这个改为动态规划,即迭代n轮,每一轮对n^2个数进行更新,在每一轮对每一个数更新需要O(1)时间,所以总的时间为O(n^3).

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值