赞
踩
在之前文章中的逻辑结构介绍中,已经知道了图是一种多对多的关系。接下来就是一些图的概念介绍和它们的一些使用即相关题目。
图G由顶点集和顶点间的关系集合(边集)E组成,记为G=(V,E),V(G)为图中顶点的有限非空集,E(G)为图中边的有限集合。如下图。
那么上图的点集和边集如下:
点集:V={A,B,C,D,E}
边集:E={(A,B),(A,D),(A,E),(B,D),(C,D),(D,E)}
因为是无向图,所以用小括号表示。
若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v,w>,其中v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自v。
若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的无序对,记为(v,w)或(w,v)。
下面两个,前者是无向图,后者是有向图。
无向图顶点A到D可以写成(A,D),有向图顶点A到顶点D就可以写成<A,D>。这有点类似于标量和矢量一样。这里可能有些人对弧尾和弧头会产生混乱,只要记住箭头指向的是弧头,另一端则是弧尾。
在这里,我们也可以给边加上权,比较A到B可以加上3,这里的权跟树中一样,可以理解为花费的时间或者路程等。
这里的度和树中的度的概念比较相似。
无向图的度是指依附于某一顶点的边的个数为那个顶点的度。举个例子,下面这个无向图A的度为3。无向图中,所有顶点度的和为边的二倍,因为每个顶点的边会被重复计算一次,比如从A到B,然后B到A都是同一条边。
有向图的度是依附于某一顶点的弧的个数为这个顶点的度,顶点的度=顶点的入度+顶点的出度。举个例子看下图,顶点A的度是3,因为A有3个指向别人的线,那么就是出度为3,没有入度。或者看C,C的入度为2,因为有2个指向它的弧,C没有出度,所以C的度就是2。所有顶点入度的和=所有顶点出度的和=边。
完全图分为无向完全图和有向完全图。无向完全图针对无向图,它是任意两个顶点之间都存在边。有向完全图是任意两个顶点之间都存在方向相反的弧。可以看下下面两张图。
那么这里要知道如果有n个顶点的无向完全图和n个顶点的有向完全图它们有多少条边呢。
先来看无向完全图,已知如果一共有n个顶点,那么一个顶点它就连n-1条边,那么一共有n个顶点,所以是
n
∗
(
n
−
1
)
n*(n-1)
n∗(n−1),但是边重复计算了,所以要除以2,那么n个顶点的无向完全图有
n
∗
(
n
−
1
)
2
\frac{n*(n-1)}{2}
2n∗(n−1)条边。这可以直接用组合C(2,n)。
有向完全图的话,则可以用A(2,n)。如果对排列组合不太明白的话可以看下排列组合这篇文章。或者也可以用另一种,每一个顶点的话可以有2(n-1),那么一共n个顶点就是2(n-1)*n,因为边被重复算了,所以要除2,最后得到
n
∗
(
n
−
1
)
n*(n-1)
n∗(n−1)。
先看下原图。
子图的边(弧)集和顶点集都为原图的子集。
生成子图的边(弧)集为原图的子集,顶点集=原图顶点集。(这里要注意顶点一定要是原图顶点集)
无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。如下图。
在有向图中,只有强连通这个概念,强连通就是顶点v要能走到顶点w,顶点w也要能走到顶点v,这样称顶点v和顶点w是强连通的。如下图。
怎么样的无向图才叫连通图,就是任意两个顶点都是连通的,叫做连通图。如下图。
任意两个顶点都是强连通的图称为强连通图。如下图。
无向图的连通图最少有n-1条边。如下图。
强连通图最少有n条边。
连通分量:无向图G的极大连通子图。极大就是再增加一个顶点就不满足连通图了。
上图不是一个连通图,因为C和其他结点没有连通,所以这是一个非连通图,那么这个图的连通分量是下图。
对这一个连通图,有两个连通分量。
有向图只有强连通分量,就是G的极大强连通子图。比如对下面这个有向图。
这是一个强连通图。如果这个图本身是强连通的了,那么这个图的强连通分量就是它自己,与无向图的连通分量的意思一样。
强连通子图并不一定是强连通分量,强连通分量一定是极大的。
生成树只针对无向图。
无向图中连通图的生成树:包含连通图G所有点集的极小连通子图。(极小:不能再删除边了)
n个顶点,n-1条边。
那么我们看下下图的生成树是什么。
上面这两个都可以称为这个连通图的生成树。
当边数小于n-1条,那一定不连通。当边数大于n-1条边的时就有环或者说回路。如果说有n-1条边,不一定是生成树。
生成树是对连通图来说的,那么对于非连通图就有生成森林。
非连通图的每个连通分量的极小连通子树(生成树)构成的森林。如下图。
那么取这个非连通子图的连通分量。如下图。
然后对这两个连通分量取他们的生成树。
因为C没有边,所以并不需要。
有向图的生成森林:若干棵有向树构成的森林。
有向树:只有一个顶点入度为0,其余顶点入度均为1。
那么上图就是有向图的生成森林。
带权的连通图(包括弱连通的有向图)成为网。
弱连通的有向图就是将有向图的“矢量”变成“标量”,把有向图画成无向图的形式都能连通,这样称为是弱连通的有向图。
这里比如第二个有向图B到D,那么就把B->A->D称为路径,没有重复的顶点,这样称为简单路径。
如果从B->A->D->B,从B又到B,这样就不叫简单路径,这样就是环或回路了。
如果除了第一个和最后一个,其余没有重复的顶点了,这样称为简单环或简单回路。
根据一些题目来对上述内容进行一个小结,回顾一下内容。
【例】已知无向图G含有16条边,其中度为4的顶点个数为3,度为3的顶点个数为4,其他顶点均小于3。图G所含的顶点个数至少是____。
解:之前有提到过边和度数的关系。
∑
所
有
度
=
2
∗
边
数
{\sum}所有度=2*边数
∑所有度=2∗边数,边数固定,想要顶点越小,那就让所有的度越大越好。那么就让其他的度都为2,得到公式
4
∗
3
+
3
∗
4
+
2
∗
x
=
2
∗
16
4*3+3*4+2*x=2*16
4∗3+3∗4+2∗x=2∗16,那么得到x=4。所以顶点个数最少应该是11个。
【例】若无向图G=(V,E)中含有7个顶点,要保证图G在任何情况下都是连通的,则需要的边数最少是_____。
解:我们知道,如果是完全无向图,那就有
n
∗
(
n
−
1
)
2
\frac{n*(n-1)}{2}
2n∗(n−1),所以最多只能有21条边。那么我们就考虑,如果是6个顶点的完全无向图,最大边数是15,如果是加一条,那么就放不进去,所以这时候再来一个顶点只需要带上一条边就行,因为6个顶点的已经是完全无向图了,这样情况下保证都是连通的,如果是15的话,那么6个顶点构成完全无向图,那么有1个顶点就是孤立的就不行。所以结果是16。
最后用两张图来总结下这些。
我们可以通过一张思维导图来了解下本部分需要了解的一些图的存储结构。
图G由顶点集和顶点间的关系集合(边集)E组成,记为G=(V,E),V(G)为图中顶点的有限非空集,E(G)为图中边的有限集合。
那么上图的点集和边集如下:
点集:V={A,B,C,D,E}
边集:E={(A,B),(A,D),(A,E),(B,D),(C,D),(D,E)}
从这里,我们可以看出点集可以用一维数组表示,而边集可以用二维数组表示。如下图所示。
邻接矩阵何时为1,何时为0,就是看这两个顶点之间有没有连接,如果用公式表达的话就是用下图。
这里的顶点表一般是从下标0开始的,那么A就是0,B就是1,邻接矩阵下标也是从0开始,那么(0,0)就是顶点A到顶点A,可见是没有连线的所以是0,那么(0,1)就是A到B,发现是有的,那就是1。
其实无向带权图理解起来很简单,本来无向无权图中两个顶点连线都1,那么带权就是将1变成那个带的权就行,0的话可以写成
∞
\infty
∞,理解成这两个顶点是没边的。那么看下图。
它的顶点表和邻接矩阵如下。
同样给出公式表示邻接矩阵中的数字是如何得到的,注意下面的E就是边集,就是表示这两个顶点之间有关系。
因为如何通过邻接矩阵来判断某个顶点的度,比如可以看A顶点,那么就看第一行,发现3个地方有权重,那么度就是3,其余同理。所有的度的和就是2*边数,那么我们发现这个其实是对称矩阵,所以存储就可以上三角或者下三角。我们可以知道上三角有多少个位置是有值的,那么这个边数就是多少。
下面来看下无向图的邻接矩阵的特性:
1.一定是对称矩阵,因此按照压缩矩阵存储,可以只存放上三角/下三角元素。
2.对于顶点
v
i
v_i
vi,度数是第i行/第i列的非0/
∞
\infty
∞元素的个数。
3.无向图的边数是上三角/下三角矩阵中非0/
∞
\infty
∞元素的个数。
有向无权图的邻接矩阵其实和无向无权的概念差不多,都是如果
v
i
v_i
vi到
v
j
v_j
vj有边,那么就
(
i
,
j
)
(i,j)
(i,j)就是1。来看下下图。
那么这个图的顶点表和邻接矩阵如下图。
有向无权跟无向无权不一样,它不是一个对称矩阵了。同样我们可以看下它的公式。
这里如果看第一行,第一行是A,那么有1个值,那么就是A的出度为1,这里就是看从A出去,那么看第一列,也是A,不过这里是A的入度,有2个,所以A的度为3,就是它的行的值的个数+列的值的个数。
这里跟上面的一样,把1换成权值,把0换成 ∞ \infty ∞,就不过多说明。
那么有向图的邻接矩阵的特性如下:
1.对于顶点
v
i
v_i
vi,第i行的非0/
∞
\infty
∞元素个数为其出度,第i列的非0/
∞
\infty
∞元素个数为其入度。
2.邻接矩阵中非0元素的个数就是图的弧的数目。
一般邻接矩阵的话都是用来存储稠密图,如果是稀疏图的话一般用邻接表来存储。稀疏图就是1比较少,0比较多,也就是边少,这样用邻接矩阵会比较浪费空间。
邻接表法:为每一个顶点建立一个单链表存放与它相邻的边。
顶点表:采用顺序存储,存放顶点的数据和边表的头指针。
边表:采用链式存储,存放与一个顶点相邻的所有边。
顶点表和边表如下图。
顶点表中:data,顶点域。firstarc,边表头指针。
边表中:adjvex邻接点域,nextarc,指向下一条邻接边的指针域。
那么通过实例来了解下。如下图。
那么顶点表如下图。
那么它的邻接表如下图:
我们发现A连接是B、D和E,那么B下标是1,D下标是3,E下标是4,所以边表跟的就是1、3、4。其余几个都一样的道理。那么我们可以横着看,它边表的结点的个数就是这个顶点的度。
当然除了无向图的话,还有有向图的邻接表,那么上面这个有向图的邻接表就如下所示。
这里的边表的话是有方向的,也可以叫做出边表,比如A指向D,那么就是A的出度是D这个顶点,那么就把D放到后面,C是没有出度的,所以C后面指向的就是NULL。这里邻接表,出边表的个数就是图的边的个数。
最后来总结下邻接表的一些特性:
1.表头向量中每个分量就是一个单链表的头结点,分量个数为图中的顶点数。
2.在边或弧稀疏的条件下,用邻接表比邻接矩阵更节省空间。
3.无向图中,顶点
v
i
v_i
vi的度是第i个链表的结点数。
4.有向图可以建立正邻接表/逆邻接表:
(1)正邻接表:出边表。
(2)逆邻接表:入边表。
5.有向图中,第i个链表的结点数是顶点
v
i
v_i
vi的出/入度;求出/入度,必须遍历整个邻接表。
为了更加直观的表明有向图的出度和入度,那么就引入了十字链表。十字链表有顶点表和弧表,如下图。
顶点表中有三个项,data域,存放顶点相关的数据信息,如顶点名称。firstin和firstout两个域分别指向以该顶点为弧头或弧尾的第一个弧结点。
弧表有5个域:尾域tailvex和头域headvex分别指示弧尾和弧头这两个顶点在图中的位置;链域hlink指向弧头相同的下有一条弧;链域tlink指向弧尾相同的下一条弧;info域指向该弧的相关信息。这样,弧头相同的弧就在同一个链表上,弧尾相同的弧也在同一个链表上。还是直接通过例子来理解。
对于上面这张图,它的顶点表如下。
看下A有多少条出边,那么只有1个,那就是A指向D,也据说0指向3。那么如下图。
同样的,我们将B,C和D,三个的都先加上。B的话只有到A,C的话到A也到D,D的话到B。结果如下。
那么,我们就先将每个顶点的出度就表示好了,也就是顶点表的弧尾。接下来就要解决顶点表的firstin了,也就是顶点的入边,那么我们可以知道A的入边应该是1->0,2->0。所以应该将A连给1->0,再用1->0的hlink连2->0,因为2->0之后没有A的入度了,所以将2->0的hlink设置为NULL,如下图。
其余也是同理操作,那么得到最后的图如下。
从一个顶点的firstout出发,沿表结点的tlink指针构成了正邻接表的链表结构。
从一个顶点的firstin出发,沿表结点的hlink指针构成了逆邻接表的链表结构。
邻接多重表的是对无向图的。下面是邻接多重表的顶点表和边表。
其中顶点表中,data域存储该顶点的相关信息,firstedge域指示第一条依附于该顶点的边。
边表中,mark为标志域,可以用以标记该条边是否被搜索过;ivex和jvex为该边依附的两个顶点在图中的位置;ilink指向下一条依附于顶点ivex的边;jlink指向一条依附于顶点jvex的边,info为指向和边相关的各种信息的指针域。
用一个例子来理解下,如下图。
顶点表如下。
对于每一个顶点,先取它的第一条边,比如A的第一条是AB,B的是BC。那就会得到如下图。
这里用了除了mark和info之外的四个域名。
那么A除了B之外,A还有C和D,那么就用到了0->1的ilink了。找依附0的边,那么就可以直接指向3->0。我们还有2->0,那么就加上2->0,这是依附于0的边,所以用3->0的jlink去指向2->0。那么到此A的依附的边都找到了。
现在的话就是找B依附的边,B的边是1->2,那么一开始已经将1->2加入了,所以要找另一条1->0,那么从1->2的ilink往上就是1->0这条边。所以用1->2的ilink指向0->1。
C的话边是2->3,1->2和0->2,那么就用2->3的ilink去指向1->2,再用1->2的jlink去指向2->0即可。
D的话就是3->0和2->3,同理3->0是它本身,那么2->3是上面那个,所以要用3的ilink去指向2->3。
最后得到的邻接多重表如下。
那么来看下王道书上的例子吧。
图的遍历有两种模式,一种是广度优先搜索,一种是深度优先搜索。
图的遍历是从图的某个顶点出发,按照某种搜索方法沿着图的边访问图中的其余顶点,且每个顶点仅被访问一次。这里会结合邻接表来看下两种遍历的时间复杂度。
遍历逻辑:
1.从图中某顶点
v
i
v_i
vi出发,访问
v
i
v_i
vi;然后找到
v
i
v_i
vi的一个邻接顶点
v
i
1
v_{i1}
vi1;
2.从
v
i
v_i
vi出发,深度优先搜索访问和
v
i
1
v_{i1}
vi1相邻且未被访问的所有顶点。
3.转(1),直到和
v
i
v_i
vi相邻接的所有顶点都被访问为止。
通过下图举个例子:
先访问A,那么与A相紧邻的是B,访问B,再访问B相紧邻的未被访问的,那么A是被访问的,所以访问D,再访问D相紧邻的C和E,然后返回A,看与A相紧邻的有没有没被访问的,发现没有了,那就结束。
对于有向图也是一样的道理。先访问A,A相紧邻的只有D,因为从A出去只能到D。那么和D相紧邻的是B和C,先访问B,然后找B相紧邻的,发现A被访问了,那么退到D,再去访问C,最后访问E。所以应该是ADBCE。
深度优先遍历有点类似于先序遍历,类似于从一个地方开始一直找到结束。
看下用邻接表来存储,搜索的复杂度是多少,看下下面的图。
顶点是n,边表是2e,那么用大O表示法就是O(n+e)。
在有向图中也一样,看下图。
如果顶点表是n,边表是e,那么总的复杂度也是O(n+e)。
在深度优先搜索实现方式是:一个访问标记数组 + 一个栈
上面是它的访问数组,为什么这里采用的是栈呢,因为它会对一个顶点访问到底,然后才回进行一个一个返回,比如上图中,先将A访问压入,然后将B访问压入,再D访问压入,那么看D紧邻的,发现B和A已经访问过了,所以只需要入栈C或者E就行,比如先访问C,入栈C,然后发现C的紧邻是D,D已经访问了,所以C就没有未被访问的入栈,那就弹出C,回到D,再访问E,入栈E,那么到此E的紧邻也访问完,依次弹出,其实操作都一样了,所以采用栈比较好实现。
深度优先搜索递归形式的代码比较简单,如下:
bool visited[MAX_VERTEX_NUM];//访问标记数组 void DFSTraverse(Graph G){//对图G进行深度优先遍历 for(v=0;v<G.vexnum;++v) visited[v]=FALSE;//初始化标记数组 for(v=0;v<G.vexnum;++v) if(!visited[v]) DFS(G,v); } void DFS(Graph G,int v){//从顶点v出发,深度优先遍历图G visit(v);//访问顶点v visited[v]=TRUE;//设已访问标记 for(w=FirstNeighbor(G,v);w>=0;w=NextNeighor(G,v,w)) if(!visited[w]){//w为u的尚未访问的邻接顶点 DFS(G,w); } }
遍历逻辑:
1.从图中某顶点
v
i
v_i
vi出发,访问
v
i
v_i
vi。
2.访问
v
i
v_i
vi所有相邻且未被访问的所有顶点
v
i
1
,
v
i
2
v_{i1},v_{i2}
vi1,vi2
3.以
v
i
1
,
v
i
2
v_{i1},v_{i2}
vi1,vi2的顺序,对每个顶点执行(1)(2),直到所有顶点被遍历。
通过下面这个无向图来举例。
首先从A开始,那么A相邻的就是BDE,那么BDE有哪些没有被访问的,A和D已经访问了,那么就访问C,所以遍历序列就是ABDEC。这个有点类似于二叉树的层次遍历,就像从根开始,然后访问根的孩子,然后访问孩子的孩子一样的道理。
对于有向图来说,也是一样的逻辑,比如下图。
不过这里要注意从A开始的话,只有D是A的邻接的,因为A只能走到D。所以先是AD,那么D能访问B和C,最后才通过C访问E,那么结果就是ADBCE这样。
我们应该了解,在邻接表的形式中是如何遍历的,如下邻接表。
邻接表的话有顶点表和边表。如果有n个顶点,那么顶点表就有n个结点。如果边有e条,那么边表中应该有2e个,因为每条边会重复计算一次。那么广度优先就是通过顶点表中的顶点去访问边表的样子。对A顶点,那么先访问A,再访问A的三个边表,其余几个也一样。所以遍历的量级应该是顶点表+边表的,所以是O(n+2e),所以是O(n+e)。
如果是对上面那个有向图,它的邻接表为下图。
顶点表是n个结点,出边表如果有e条边,那么结点个数就是e。遍历量级就是O(n+e)。
在广度优先表中,实现方式是:一个访问标记数组+一个队列。
如果访问了一个结点就让visit[i]=1。模拟访问过程大家可以自己试一下。
最后来看一下BFS的伪代码
bool visited[MAX_VERTEX_NUM];//访问标记数组 void BFSTraverse(Graph G){//对图G进行广度优先遍历 for(i=0;i<G.vexnum;++i){ visited[i]=FALSE; //访问标记数组初始化 } InitQueue(Q);//初始化辅助队列Q for(i=0;i<G.vexnum;i++)//从0号顶点开始遍历 if(!visited[i])//对每个调用一次BFS BFS(G,i);//vi未访问过,从vi开始BFS } void BFS(Graph G,int v){//从顶点v出发,广度优先遍历图G visit(v);//访问初始顶点v visited[v]=TRUE;//对v做已访问标记 Enqueue(Q,V);//顶点v入队列Q while(!isEmpty(Q)){ DeQueue(Q,v);//顶点v出队列 for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){ //检测v所有的邻接点 if(!visited[w]){//w未v的尚未访问的邻接点 visit(w);//访问顶点w visited[w]=TRUE;//对w做已访问标记 EnQueue(Q,w);//顶点w入队 } } } }
小结部分还是用题目来回顾前面的知识点。
【例】设有向图G=(V,E),顶点集V={
V
0
,
V
1
,
V
2
,
V
3
V_0,V_1,V_2,V_3
V0,V1,V2,V3},边集E={
<
v
0
,
v
1
>
,
<
v
0
,
v
2
>
,
<
v
0
,
v
3
>
,
<
v
1
,
v
3
>
<v_0,v_1>,<v_0,v_2>,<v_0,v_3>,<v_1,v_3>
<v0,v1>,<v0,v2>,<v0,v3>,<v1,v3>}。若从顶点
v
0
v_0
v0开始对图进行深度优先遍历,则可能得到的不同遍历序列个数是。
解:首先,先画一下这张图。
从
v
0
v_0
v0开始的话,第一个是
v
0
v_0
v0肯定是没错的,那么第二个可能是什么,第二个可能是
v
1
v_1
v1,可能是
v
2
v_2
v2,可能是
v
3
v_3
v3,这里就出现三种了。
那么从第二个
v
1
v_1
v1开始,先
v
0
v_0
v0,再
v
1
v_1
v1,
v
1
v_1
v1相邻的是
v
3
v_3
v3,那么访问完返回
v
0
v_0
v0,最后访问
v
2
v_2
v2。序列就是
v
0
,
v
1
,
v
3
,
v
2
v_0,v_1,v_3,v_2
v0,v1,v3,v2。
那么从第二个是
v
2
v_2
v2开始,第一个还是
v
0
v_0
v0,第二个
v
2
v_2
v2,那么返回
v
0
v_0
v0,这时候有两个选择,一个是
v
1
v_1
v1,一个是
v
3
v_3
v3,如果第三个先访问
v
3
v_3
v3,那么访问完需要返回
v
0
v_0
v0,然后再访问
v
1
v_1
v1,如果先访问
v
1
v_1
v1,那么可以访问完
v
1
v_1
v1直接访问
v
3
v_3
v3。所以这里有两个序列,分别是
v
0
,
v
2
,
v
1
,
v
3
v_0,v_2,v_1,v_3
v0,v2,v1,v3和
v
0
,
v
2
,
v
3
,
v
1
v_0,v_2,v_3,v_1
v0,v2,v3,v1。
同样从第二个
v
3
v_3
v3开始也有两个序列,这里可以自己推下,分别是
v
0
,
v
3
,
v
1
,
v
2
v_0,v_3,v_1,v_2
v0,v3,v1,v2和
v
0
,
v
3
,
v
2
,
v
1
v_0,v_3,v_2,v_1
v0,v3,v2,v1。
所以一共是5种。
【例】
解:这个其实只要一个一个看下来就行,A的话是先访问h,那么h邻接的是c和a,c是bd,a是e,e是g和f,所以A正确,其他同理,只有D是错误的。
首先,通过一个思维导图来了解下这部分的大致内容。
最小生成树中的生成树其实在之前的内容中也有接触过了,那么再来回顾下这个内容。
无向图中连通图的生成树:包含连通图G所有点集的极小连通子图。(极小:并不能再删除边)
n个顶点,n-1条边。比如下面右边两个都是最左边的生成树。
那么最小生成树的话就需要加上权值,概念是:如果(无向)连通图是一个带权图,则其生成树中所有边的权值之和为生成树的代价,带权连通图中代价最小的生成树为最小生成树(Minimum Spanning Tree)。还是上面这张图,如果给他们都加上权值,可以发现最右边的权值总和最小。
最小生成树有以下性质:
1.最小生成树不唯一,只有图中的各边权值互不相等时才唯一。
2.图的各边的权值有相等情况时,最小生成树可能不唯一,但最小生成树的代价一定唯一。
3.最小生成树有n个顶点,n-1条边,且无回路。
算法步骤如下:
1.任意选一个顶点,找与他相邻的权值最小边,保留该边所依附的顶点。
2.找已有顶点集邻接顶点中权值最小边(但不能构成环路),保留该边所依附的顶点;
3.循环(2),直到找到所有顶点。
可能通过上述文字理解的不是很清楚,那么通过下面这个例子来理解下。
那么从A顶点开始,A的相邻的权值最小边是A->D,权值是1。如下图所示。
那么在已有顶点集,也就是A和D,再找权值最小,且不构成环路,那就是D->B,权值为2,这里也可以选择A->B,因为权值也为2。如下图(选了B->D)。
循环第二步操作,因为A->B会构成环路,所以不能选A->B,那么选A->E,保留E,最后连C->E,得到最小生成树,如下图。
克鲁斯卡尔算法如下:
1.初始所有的顶点,将边的权值由小到大排序,挑选最小权值放入最小生成树中;
2.循环(1),但不能构成环路,直至添加了n-1条边,循环结束。
还是用下图来举例。
首先初始化所有顶点,然后将边的权值由小到大,那么最小是1,所以先连A->D,再是2,可以连A->B或者B->D,但是不能两个都连,这样就构成了环路。接下来连C->E,A->E。这样就添加了n-1条边,完成,得到下图。
用一道题目来回顾下这部分的内容。
【例】
解:用克鲁斯卡尔的话,会先选择最小权值的,那就是
v
1
,
v
4
v_1,v_4
v1,v4,权值为5,那么第二选择的是权值为8的了。那就是
<
v
1
,
v
3
>
<v1_,v_3>
<v1,v3>,
<
v
3
,
v
4
>
<v_3,v_4>
<v3,v4>或者
<
v
2
,
v
3
>
<v_2,v_3>
<v2,v3>。所以答案在A,B,D中,那么通过普里姆来看,如果是
v
4
v_4
v4的话,那么
<
v
1
,
v
4
>
<v_1,v_4>
<v1,v4>最短先连接,那么通过
v
1
v_1
v1和
v
4
v_4
v4顶点选择,可以连
<
v
1
,
v
3
>
<v_1,v_3>
<v1,v3>也可开
<
v
3
,
v
4
>
<v_3,v_4>
<v3,v4>,所以可以直接得到答案是C,
<
v
2
,
v
3
>
<v_2,v_3>
<v2,v3>不会被第二次选中。
无权图最短路径:A顶点到B顶点边数最短的路径,该路径长度/边数为最短路径长度或最短距离。
带权图最短路径:A顶点到B顶点权值和最短的路径,该带权路径长度/权值和为最短路径长度或最短距离。
拿下面两张图来说。
这里A到C是无法直接到达的,那么得从A到D到C,也就是路径是2,当然A到E到C也是2,所以这两个都是最短路径。(这里其实可以把无权图理解为权值都为1)
还是A到C,如果是A->D->C,那么权值和是6,如果A->E->C,那么权值和是7,那么最短路径是A->D->C,最短路径长度是6。
目标:找到某顶点到其他各顶点的最短路径。
这里假设是从顶点A开始,那么构建下面这张表,分别是从A到各个顶点,S的话是看当前顶点有没有在顶点集中,0表示未在,1表示在。dist表示顶点到当前顶点的距离。首先来下面这张表,是顶点A到其他顶点的距离。
A | B | C | D | E | F | |
---|---|---|---|---|---|---|
S(0/1) | 0 | 0 | 0 | 0 | 0 | 0 |
dist | 0 | ∞ \infty ∞ | 3 | 9 | ∞ \infty ∞ | ∞ \infty ∞ |
这时候,先将A加入进来得到下面这张表,发现A加入并不改变距离。 | ||||||
A | B | C | D | E | F | |
– | – | – | – | – | – | – |
S(0/1) | 1 | 0 | 0 | 0 | 0 | 0 |
dist | 0 | ∞ \infty ∞ | 3 | 9 | ∞ \infty ∞ | ∞ \infty ∞ |
那么看下剩下的这里面,谁的权值最小,发现是3,那么把3的C加入到顶点集。加入C后,发现可以从A->C->D,那么权值从9变成了7。并且可以走到E了,是A->C->E,权值是16。也可以走A->C->F,得到18。 | ||||||
A | B | C | D | E | F | |
– | – | – | – | – | – | – |
S(0/1) | 1 | 0 | 1 | 0 | 0 | 0 |
dist | 0 | ∞ \infty ∞ | 3 | 7 | 16 | 18 |
接下来D权值最小,那么将D加入到顶点集。 | ||||||
A | B | C | D | E | F | |
– | – | – | – | – | – | – |
S(0/1) | 1 | 0 | 1 | 1 | 0 | 0 |
dist | 0 | ∞ \infty ∞ | 3 | 7 | 16 | 18 |
发现A还是不能通过D到B,所以B还是 ∞ \infty ∞。那么加入D后,就可以走A->C->D->E这样,就是权值12。所以得到下表。 | ||||||
A | B | C | D | E | F | |
– | – | – | – | – | – | – |
S(0/1) | 1 | 0 | 1 | 1 | 0 | 0 |
dist | 0 | ∞ \infty ∞ | 3 | 7 | 12 | 18 |
现在是E最小,那么将E加入顶点集。那么可以通过A->C->D->E到F,得到权值16,所以得到下表。 | ||||||
A | B | C | D | E | F | |
– | – | – | – | – | – | – |
S(0/1) | 1 | 0 | 1 | 1 | 1 | 0 |
dist | 0 | ∞ \infty ∞ | 3 | 7 | 12 | 16 |
这样就得到了从A顶点到其他顶点的最短路径,它本质上还是贪心的算法,求的局部最优解,至于什么是贪心,到时候会有另一篇文章来讲述。 |
目标:找到任意两个顶点间的最短路径。
如果是用邻接矩阵来存储的话,如下图:
[
0
2
8
∞
0
4
5
∞
0
]
\left[
先加入A顶点。那么加入A之后发现C可以到B了,就是C->A->B,权值为7。
[
0
2
8
∞
0
4
5
7
0
]
\left[
现在加入B顶点,那么A到C本来是8,加入B就可以从B走,变成2+4=6,所以得到下矩阵。
[
0
2
6
∞
0
4
5
7
0
]
\left[
那么加入C顶点,B就可以通过C到A,4+5=9。得到下矩阵。
[
0
2
6
9
0
4
5
7
0
]
\left[
上面两个算法都是直接通过例子来进行操作了,没有写太多步骤,而是将步骤融入到例子里面,相信通过例子可以直接理解,因为本文还是建立在看过一些教材的基础上直接解释的(为了节约打字= =)。
那么最后通过一道例题来结束最短路径这部分。
【例】
解:还是跟之前的一样,先建立一张表格。
1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|
S(0/1) | 0 | 0 | 0 | 0 | 0 | 0 |
dist | 0 | 5 | ∞ \infty ∞ | ∞ \infty ∞ | 4 | ∞ \infty ∞ |
那么先加入1,是不改变的,那么顶点5的权值是最小的,将5加入。可以得到下表。 | ||||||
1 | 2 | 3 | 4 | 5 | 6 | |
– | – | – | – | – | – | – |
S(0/1) | 1 | 0 | 0 | 0 | 1 | 0 |
dist | 0 | 5 | ∞ \infty ∞ | 11 | 4 | 9 |
现在顶点2最小,将2加入,那么就可以通过2到3,得到7。到4的话是5+9但是大于14不更新。得到下表。 | ||||||
1 | 2 | 3 | 4 | 5 | 6 | |
– | – | – | – | – | – | – |
S(0/1) | 1 | 1 | 0 | 0 | 1 | 0 |
dist | 0 | 5 | 7 | 11 | 4 | 9 |
这里可以得到,顶点应该是顶点5,顶点2,顶点3,所以从A和B里面选。按照上面的继续往下,先加入权值小的顶点6,最后是顶点4,所以选择B。 |
在开始拓扑排序之前,先来了解下AOV网。
AOV网:用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网(Activity On Vertex Network),简称AOV网。在网中,若从顶点i到顶点j有一条有向路径,则i是j的前驱。
AOV网是一种有向图无回路的图。
拓扑排序算法思想:
1.AOV网中选择一个没有前驱的顶点并输出;
2.删除该顶点及其出发的所有弧。
3.重复(1)(2),知道图中全部顶点都输出(图中无环)或图中不存在无前驱的顶点(图中必有环)。
若邻接矩阵是三角矩阵,则存在拓扑排序。(因为比如是上三角,那么i≤j,如果有i,j,k三个顶点。那么
a
i
j
a_{ij}
aij和
a
j
k
a_{jk}
ajk还有
a
k
i
a_{ki}
aki之间,知道i≤k,k≤i,矛盾了,大家可以自己画一下)
看上图中,发现B是没有前驱的,那么就输出B,然后删除B出发的所有弧,得到如下图。
现在重复(1)(2)那么可以知道,现在A是没有前驱的,那么输出A,并删除A依附的出边,得到如下图。
接下来是C,因为C没有前驱,所以删除并输出C,还有删除C的出边,得到下图。
接下来操作都一样,删掉D,删掉E,最后删掉F。所以它的拓扑排序是:BACDEF。
如果是环的话,每一个结点既有前驱又有后继,那就不存在满足(1)
的条件了,那么可以确定这不是一个AOV网,因为是有回路了。
那么最后有一道题目来回顾下这部分的内容。
解:这题B、C和D第一个都是5,那么就从这几个开始着手,如果先删除5后,就要删除5->2和5->4,那么发现没有前驱的只有1,所以第二个只能输出1,而不能输出2,所以马上就可以发现D是不可以的了。那么答案就是D。其余几个可以自己尝试一下。
这个应用有点偏工程的应用,那么在开始关键路径之前,来了解一下AOE网。
AOE网:有向带权图中,以顶点表示事件,以有向边表示活动,以边上权值表示完成该活动的开销(如完成活动所需要的时间),则称这种有向图为用边表示活动的网络,简称AOE网。在这个网中有两个点,一个是源点(入度为0),一个是汇点(出度为0)。
关键路径:从源点到汇点最大路径长度的路径为关键路径,该路径上的活动(边)为关键活动。
那么我们来看下,最早什么时候到E,这里要以最晚的为主,A到B到E是4,A到C到E是6,那么E的最早开始时间就是6。同样可以通过这样来算出其他几个顶点的最早开始,比如D的话是3+2=5,F的话是2+4+5=11(其余几个加起来也是11)。
看下某个事件最晚什么时候开始。比如我们知道图中F最早开始时间是11,那么就可以通过11往前推,得到D的最晚开始时间是11-6=5。E的最晚开始时间是6,其余推理同理。有以下公式:
v
l
(
汇
点
)
=
v
e
(
汇
点
)
vl(汇点)=ve(汇点)
vl(汇点)=ve(汇点)
v
l
(
k
)
=
M
i
n
v
l
(
j
)
−
W
e
i
g
h
t
(
v
k
,
v
j
)
vl(k)=Min{vl(j)-Weight(v_k,v_j)}
vl(k)=Minvl(j)−Weight(vk,vj),
v
k
v_k
vk为
v
j
v_j
vj的任意前驱。
它是指该活动弧的起点所表示的事件最早发生时间。若边
<
v
k
,
v
j
>
<v_k,v_j>
<vk,vj>表示活动
a
i
a_i
ai,则有
e
(
i
)
=
v
e
(
k
)
e(i)=ve(k)
e(i)=ve(k)。
它是指该活动的弧的终点所表示事件的最迟发生事件与该活动所需时间之差。若边
<
v
k
,
v
j
>
<v_k,v_j>
<vk,vj>表示活动
a
i
a_i
ai,则有
l
(
i
)
=
v
l
(
j
)
−
W
e
i
g
h
t
(
v
k
,
v
j
)
l(i)=vl(j)-Weight(v_k,v_j)
l(i)=vl(j)−Weight(vk,vj)
比如要求
a
5
a_5
a5这个活动的最迟开始时间,先得看D的最迟开始时间再减去
a
5
a_5
a5的权值。
它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动
a
i
a_i
ai可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称
l
(
i
)
−
e
(
i
)
=
0
l(i)-e(i)=0
l(i)−e(i)=0即
l
(
i
)
=
e
(
i
)
l(i)=e(i)
l(i)=e(i)的活动
a
i
a_i
ai是关键活动。
求关键路径的算法步骤如下:
1)从源点出发,令
v
e
(
源
点
)
=
0
ve(源点)=0
ve(源点)=0,按拓扑有序求其余顶点的最早发生时间
v
e
(
)
ve()
ve()。
2)从汇点出发,令
v
l
(
汇
点
)
=
v
e
(
汇
点
)
vl(汇点)=ve(汇点)
vl(汇点)=ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间。
3)根据各顶点的
v
e
(
)
ve()
ve()值求所有弧的最早开始时间
e
(
)
e()
e()
4)根据各顶点的
v
l
(
)
vl()
vl()值求所有弧的最迟开始时间
l
(
)
l()
l()
5)求AOE网中所有活动的差额
d
(
)
d()
d(),找出所有
d
(
)
=
0
d()=0
d()=0的活动构成关键路径。
下面通过一道题目来巩固下这部分内容。
解:先要通过关键路径的步骤来找关键路径。
(1)从源点出发,各顶点的最早发生时间如下图:
顶点 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
v e ( i ) ve(i) ve(i) | 0 | 12 | 8 | 21 | 18 | 27 |
(2)从汇点出发,找最迟发生时间 v l vl vl | ||||||
顶点 | 1 | 2 | 3 | 4 | 5 | 6 |
– | – | – | – | – | – | – |
v l vl vl | 0 | 12 | 8 | 21 | 18 | 27 |
(3)根据各顶点的 v e ve ve求 e e e |
a a a | b b b | c c c | d d d | e e e | f f f | g g g | h h h | |
---|---|---|---|---|---|---|---|---|
e ( i ) e(i) e(i) | 0 | 0 | 12 | 8 | 12 | 8 | 21 | 18 |
(4)用 v l ( ) − 权 重 vl()-权重 vl()−权重 | ||||||||
a a a | b b b | c c c | d d d | e e e | f f f | g g g | h h h | |
– | – | – | – | – | – | – | – | – |
l ( i ) l(i) l(i) | 9 | 0 | 12 | 8 | 12 | 8 | 21 | 18 |
(5)找到 l ( i ) = e ( i ) l(i)=e(i) l(i)=e(i)则是关键路径。 | ||||||||
所以关键路径有三条,是bdcg、bdeh和bfh。 | ||||||||
那我们只要选一个这三个里面都包含的就行,c和a的话,a是没有的,所以A排除。d和c,第三个没有d和c,排除。f和d的话,第一个第二个有d,第三个有f,所以选C。 |
没想到每次写一篇这样的内容花费的时间超出了我的想象,无论是去看参考书或者自己理解做题都花费了一些时间,还需要将这么输出到本文,而且要简略的能节省时间去帮助应对考试的人。本文有许多不足,比如有些地方的伪代码没有加上,或者在一些地方的步骤并不详细,或者概念不全,本文意在帮助理解,而不是长篇大论把书中内容进行摘抄。如果对文中有什么地方有问题欢迎在评论区与我一起讨论,数据结构部分还有查找和排序就结束了,在这之后我也会去学习一些算法然后开始增加新的内容,一起加油吧!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。