【天勤第七章】 图算法总结

框架:

 

 

1.图的结构

(1)邻接矩阵

(1)定义邻接矩阵的数据结构,分两部分:

  • 邻接矩阵顶点类型(用来存储顶点信息比如编号、字符数据。不常用,因为编号信息意义不大,字符数据按题目要求来);
  • 邻接矩阵表(包含一个二维数组,顶点个数,边个数);

(2)先读取顶点个数n和边数e。然后对邻接矩阵的初始化,对于无权图,全部初始化为0,对于有权图的时候在循环里加一句判断,如果i==j,就赋值为0;

(3)设置三个变量v1,v2,w,分别表示从键盘或者文件中读取的两个顶点以及权值。然后根据读取的边数e做一个循环,逐条的将边值信息读取进去

(4)最后根据需要读取顶点信息。


(2)邻接表

1)定义邻接表的数据结构,分为三部分:

  • 边结点:边结点的编号、指向下一条边的指针变量;
  • 顶点:顶点信息(字符型数据)、指向顶点第一条边的指针变量;
  • 邻接表:顶点数组、顶点个数n、边数e;

2)构造一个函数创建邻接表,形参为AGraph*

3)读取顶点个数n和边数e

4)初始化顶点数组

5)根据边数,逐个读取每一条边,如果是无向图,则读进来的两个顶点都要互相插入一个边结点


(3)邻接多重表

 


(4)十字链表

 


3.图的遍历

(1)深度优先遍历递归算法(DFS)

1)构造DFS函数,类型为void,形参为邻接表AGraph*,和出发点int v,另,外部定义访问数组visit[],初始化为0;

2)定义边结点变量ArcNode* p(用来访问顶点v的每一条边);

3)访问顶点v,在访问数组visit[]中标记顶点v为已访问;

4)将顶点v的第一条边赋值给p;

5)访问顶点v中每一条边,p不为空时做循环:

  • 如果边结点没有访问过,即visit[p->adjvex] == 0,则递归深度遍历该结点
  • 否则,访问顶点v的下一条边

【技巧提示】与二叉树的前序遍历相似,只不过此处为访问与v相邻的多条边,而二叉树仅为两条边。

【注意事项】以上仅针对连通图,非连通图需要做一个循环,对逐个对每个未访问的顶点进行遍历。

(2)DFS非递归算法

1)构造返回值类型为空的函数,形参为邻接表AGraph*,出发点int v0;

2)定义变量:

  • 栈 int stack[maxSize],top = -1
  • 边结点指针 ArcNode* p
  • 标记访问数组 int visited[maxSize]
  • 循环变量i,j

3)visited数组初始化为0;

4)将出发点的访问状态标记为1,并对其访问然后入栈;

5)接下来访问剩余顶点,栈不为空时进行循环:

a.取栈顶元素,j记录父结点编号,p指向栈顶结点的第一个邻接顶点(相当于进入新的一层系统栈)

b.找到第一个未被访问过的邻接顶点:做一个while循环遍历每一条边,条件为:p != NULL并且visited[p->adjvex] == 1(相当于遍历孩子结点)

c.若p为空,则出栈top---(相当于即将离开本层系统栈,已经遍历完所有孩子结点)

d.否则,访问p所指结点,并将其入栈


(3)广度优先遍历(BFS)

1)构造深度遍历函数,形参为AGraph* G和int v,表示要访问的图和顶点v,另,外部定义访问数组visit[],初始化为0;

2)定义整型队列que,队列指针front和rear,边结点指针变量ArcNode* p,整型变量j,用来指向数组下标;

3)访问顶点v,访问数组visit[v] = 1,标记为已访问,将顶点v入队;

4)队列不为空时做一个循环1:

  • 5)队列出队,将顶点编号赋值给j,令p指向j的第一条边
  • 6)p不为空时做一个循环2:
  • 7)判断边结点是否被访问过,如果没有访问则访问它,并且将其入队,标记为已访问
  • 8)p指向结点j的下一条边
  • 9)p为空结束循环2队列为空结束循环1,遍历结束

【技巧提示】依据二叉树的层次遍历模版来记忆图的BFS,二者差别在于二叉树每次入队的是一个结点的两条边,而图是与这个结点相邻的所有未访问的边都入队。

【注意事项】以上仅针对连通图,非连通图需要做一个循环,对逐个对每个未访问的顶点进行遍历。


(3)判定顶点i和顶点j之间是否有路径

方法一:

1)访问数组visit[maxSize]初始化为0;

2)以顶点i为出发点,开始一次DFS或者BFS;

3)若visit[j]==0则二者无路径,否则有路径。

方法二:

1)以顶点i为出发点进行BFS

2)在每个顶点出队的时候判断出队顶点与j是否相等,若相等则判断有路径,结束遍历,否则继续循环

【补充】方法二的时间复杂度为O(n+e),n为访问的顶点数,e为访问的边数,最糟糕的情况下图中每个顶点都进队一次,每条边都被访问一次。(基本操作考虑双重循环的第二层,天勤书P213)


4.最小生成树

【应用背景】连通图中各顶点所需要的最少成本。

(1)普里姆算法 T195

【算法步骤】

1)根据需求构造返回类型,比如求最少成本,则定义返回类型为int型的函数,传入参数为邻接矩阵g,和出发点v0;

2)定义变量:

  • lowcost[]数组(存储图中还未并入树的各顶点到当前生成树最短边的权值),
  • vset[]数组(标记顶点是否被并入生成树中,1为真,0为假),
  • 循环变量i,j,整型变量min,k和sum(min表示生成树到剩余顶点最短边中最短的那条边的权值,k表示与这条边相连并将被并入生成树的顶点,sum表示成本总和);

3)做一个循环将lowcost数组初始化为与出发点相邻的各条边的权值,vset初始化为0;

4)将vset[v0]标记为1,表示v0并入生成树,接下来将剩余的n-1个顶点并入生成树中,即做n-1次循环:

  • 将min初始化为INF,无穷大。
  • 找出min值和k值。嵌套内循环1:执行n次,依次遍历每一个顶点,如果vset[j]为0,并且lowcost[j]<min,则分别将权值lowcost[j]和顶点编号i赋值给min和k。
  • 计算成本,sum += min,并将k标记为1。
  • 以新并入的顶点k为媒介更新lowcost数组。因为当前生成树发生了变化,剩余顶点到生成树的权值也应发生改变。嵌套内循环2:执行n次,依次遍历每一个顶点,如果vset[i]为0,并且g.edges[k][j] < lowcost[j],表示顶点j未并入生成树,且与顶点k相连的边的权值要小于顶点j到旧的生成树最短边的权值。此时说明必须要更新lowcost[]数组,将该边权值赋值g.edges[k][j] 给lowcost[j]。

5)返回sum,算法结束。

【算法分析】算法嵌套两层循环,外层循环执行n-1次,内层循环1中if语句比较n次,所以算法时间复杂度为O(n^{2})。其中n为图中顶点个数,所以普里姆算法只与图的顶点数有关,顶点越少,时间复杂度越小,因此普里姆算法适合稠密图。

(2)克鲁斯卡尔算法Kruskal T196

【算法步骤】

1)定义图中边的数据结构Road,存储连接边的两个顶点,和边的权值;

2)定义全局变量:并查集vset[],以及求根节点的函数int get_root(int a)和排序数组Sort(Road*,int);

3)确定函数返回值类型,如要求求连通最少成本,则应返回int,形参为邻接矩阵MGraph g,存储各边的数组Road road[];

4)定义变量:

  • 循环变量i,
  • 两个整型变量a,b(用来存放边的两个顶点),
  • 以及整型变量sum=0,存储总的成本;

5)初始化并查集数组,将并查集中的每一个元素视作一独立的棵树,其值为图中各个顶点的编号,此时各顶点的根结点为其自身;

6)用排序函数对数组road[]中各边按照权值从小到大排序;

7)依次遍历数组中的每一条边:

  • 取边的两个顶点a,b
  • 求顶点a所在树的根结点
  • 求顶点b所在树的根结点
  • 判断a和b的根结点是否相同,即a和b是否在同一棵树上
  • 若a和b不在一棵树上,则将a所在的树并入b中(即使a的根结点的双亲结点为b)
  • sum加上当前边的权值(也可以选择打印当前边)

【算法分析】算法的时间复杂度取决于排序函数的时间复杂度和一个单层循环,由于单层循环的时间一般是线性级的O(n),所以主要考虑排序函数的时间复杂度。而这又取决于图的边数,边数越多时间复杂度越大,反之越小。因此克鲁斯卡尔算法适用于稀疏图。


5.最短路径

(1)迪杰斯特拉算法Dijkstra T198

【算法步骤】

1)确定函数返回类型为空,形参为邻接矩阵MGraph g,出发点int v0,出发点到各终点的最短路径长度数组int dist[],以及各结点到出发点的最短路径int path[];

2)定义变量:

  • 循环变量int i,j。
  • 最小值int min,数组下标int k。
  • 访问数组int vset[],用来标记哪些顶点已经并入最短路径树。

3)初始化各个数组:

  • 数组dist初值为v0到各顶点边的权值,
  • 对于数组path,如果顶点v0到i有路径,则填入v0,表示v0到vi的最短路径上,vi的上一个结点是v0,否则填入-1
  • 对于数组vset,统一填入0

4)将vset[v0]标记为已访问,path[v0] = -1,然后遍历剩余顶点,将其并入最短路径树中:

  • min初始化为INF。(相当于擂台法求最小值)
  • 从剩余顶点中找到距离v0最近的顶点。遍历各个顶点,如果该顶点未并入最短路径树,且其dist[j]<min,则更新min和k。
  • vset[k] = 1,然后依据k作为中间点,更新dist[]数组。如果当前顶点未并入最短路径,且顶点k到当前顶点j的路径长度加上k到v0的最短路径长度小于v0到当前顶点的最短路径长度,则更新dist[j]和path[j]。即g.edges[k][j] + dist[k] < dist[j]。(这一步目的在于用刚刚并入最短路径的顶点k更新v0通往剩余各顶点的最短路径长度)

【算法分析】时间复杂度与普利姆算法相同(实际上思路也一样),都为O(n^{2})

【补充算法】打印从源点到任意顶点的最短路径上经过的顶点

1)构造返回值为空的函数,传入形参为路径数组int path[],终点int a

2)定义一个栈int stack[maxSize],top = -1以及循环变量i;

3)从叶结点到源点依次存入栈中:当a的双亲结点path[a]不为-1时,将a入栈,a的双亲结点path[a]赋值给a;

4)栈不为空时,将栈中元素依次出栈,即为源点到终点的最短路径。

(2)弗洛伊德算法 Floyd T204

【算法步骤】

1)构造返回类型为空的函数,形参为邻接矩阵MGraph g,最短路径长度矩阵A,最短路径矩阵Path;

(说明:A保存的是任意两个顶点的最短路径长度,Path存储的是任意两个顶点最短路径上的要经过的中间顶点)

2)定义变量:循环变量i,j,k;

3)矩阵A初始化为邻接矩阵g,矩阵Path初始化为-1;

4)做三个循环,k为中间点,i表示出发点,j表示终点。每次以k为媒介,更新A和Path矩阵。如果出发点i到k的路径长度加上k到终点j的路径长度小于当前两顶点i,j之间的路径长度,则进行更新。即若A[i][k]+A[k][j]<A[i][j],则令A[i][j] = A[i][k]+A[k][j]; Path[i][j] = k。

【算法分析】由于有三层循环,所以时间复杂度为O(n^{3})

【补充算法】打印任意两个顶点的最短路径

1)构造返回类型为空的递归函数,形参为int path[][maxSize],int a, int b;

2)如果path[a][b]为-1,表示没有中间点,直接输出;

3)否则,中间点k = path[a][b],先递归打印a和k的路径再递归打印k和b的路径。


(3)BFS求解单源最短路径问题 T216.16

在BFS算法上做部分修改:

1)形参增加记录树的双亲结构数组int parent[maxSize]

2)起点入队时,记录其双亲为-1,其他结点入队时,双亲结点记录为其双亲结点的编号

3)打印路径,与Dijkstra算法相同:

  • 定义一个栈
  • 做一个循环,将路径按照叶子结点到根结点的顺序入栈(while循环判断双亲结点是否为-1,不是的话将孩子结点编号入栈,再将父母结点入栈,while循环结束,将最后根结点编号入栈(因为其双亲结点编号为-1,所以不会入栈));
  • 从根节点到叶结点,逐个出栈,打印每个元素

6.拓扑排序

(1)拓扑排序算法 T207

1)修改结构体定义,在顶点结构体VNode成员内增加入度变量count,统计顶点当前的入度;

2)构造返回类型为int型的函数,形参为AGraph* g;

3)定义变量:

  • 循环变量int i,
  • 数组位置指针int j,
  • 栈int stack[maxSize],top = -1(临时存储入度为0的顶点),
  • 计数变量n(统计出栈顶点个数),
  • 边结点指针ArcNode* p;

4)循环遍历图中每一个顶点,将入度为0的顶点入栈,即删除;

5)栈不为空时,做第一层循环

出栈,将顶点编号赋值给i,n++,并输出当前顶点信息

接下来将与之相连的顶点入度减一,并将入度为0的顶点删除。p指向i的第一个边结点,p不为空时,做第二层循环

  • 将边结点编号赋值给j,将边结点对应顶点的入度减一,然后判断其的入度是否为0,如果为0则入栈
  • p指向i的下一个边结点

6)判断n是否与图中顶点个数相同,若相同说明有向图不存在回路,函数返回值为1,否则返回0。

【子算法】邻接表求顶点的入度

1)设置一个计数变量c;

2)第一层循环,逐个计算邻接表每个顶点的入度,设当前遍历的顶点为i,c初始化为0;

3)第二层循环,遍历不是i的剩余顶点,对每一个顶点再做第三层循环遍历它的边结点,判断与该顶点相连的边结点是否有i ,有则++c,并退出第三层循环(因为一个顶点的边不可能与另一个顶点相连两次),否则遍历下一条边。

4)退出到第一层循环,将c赋予相应的变量。(可以是专门的入度数组,也可以是结构体变量中的成员)

【举一反三】求逆拓扑有序序列(按照出度为0有序输出各顶点)

算法一:

1)求每个顶点的出度

2)将出度为0的顶点入栈(删除)

3)栈不为空时,做第一层循环,出度为0的顶点出栈,将与之相连的边结点出度减一(相当于删除到大该顶点的边),并将减去一之后出度为0的边结点入栈。

算法二:DFS算法求逆拓扑序列 T208 (前提是有向图无环)

最先退出算法的即为出度为0的顶点

按照退出系统栈的先后次序记录下顶点序列,即为逆拓扑序列。

算法三:最笨的方法

在掌握了拓扑有序序列的求法以后可以再球拓扑有序序列的时候设置一个栈,将拓扑有序序列顺序入栈。最后依次出栈就是逆拓扑有序序列了。

【总结】逆拓扑有序序列其实就是拓扑有序序列的逆序。看起来是废话,其实这句话可以延伸出多种求拓扑有序序列或逆拓扑有序序列的算法,考场中想出任何一种即可求解。


(2)关键路径算法

【算法核心思想】

1)求出所有事件的最早发生时间;

2)求出所有事件的最迟发生时间;

3)求出所有活动的最早发生时间;

4)求出所有活动的最迟发生时间;

5)找出所有最早发生时间与最迟发生时间相同的活动即为关键路径。


知识点归纳:

(1)一个无向图是树的条件是有n-1条边连通图

【应用补充】利用深度遍历求访问到的边数和顶点数,然后与图中的顶点数对比,需要注意的是DFS访问到的边数要除以2,因为每条边都被访问了两遍。

(2)不带权无向连通图G中距离顶点v最远的一个顶点为广度遍历的最后一个出队的顶点;

(3)什么样的图其最小生成树是唯一的?T197

【答】:图中所有边的权值各不相等,或者有相等的边,但是在构造最小生成树的过程中,相等的边都被并入生成树的图。(比如图G是一棵具有n-1条边的连通图,此时无论如何图中所有边的权值都相等)

(4)图这一章节所遇到的各类生成树总结:

广度优先遍历生成树

深度优先遍历生成树

(5)算法时间复杂度总结

1)采用邻接表的方式DFS,BFS遍历的时间复杂度为 O(n+e) T213 1(3)

2)采用邻接矩阵的方式时间复杂度为O(n^{2}) 

【延伸】由1)2)可以自然想到采用类似遍历结构的算法,如拓扑排序算法时间复杂度与之相同。

(6)图的回路问题

1)判断有向图是否有环:

2)判断无向图是否有环

并查集(应用在克鲁斯卡尔算法判断新并入的边是否会造成图出现环)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值