【课上笔记】第八章 图

8.1图的基本概念

8.1.1图的定义和术语

1.图的定义

​ 图(Graph)由一个顶点集合Vn和一个边(或者弧)集合En组成,通常记为: G=(Vn,En)其中,Vn中有n(n>0)个顶点,En中有e(e>=0)条边,且每一条边都依附于Vn中的两个顶点vi, vj(i, j=1,2,…, n),该边用顶点偶对来表示,记为( vi,vj)。

在这里插入图片描述

2.图的相关术语

⑴无向图:在一个图中,如果任意两个顶点构成的偶对(vi, vj)∈E是无序的,即顶点之间的连线没有方向,则称该图为无向图。

⑵有向图:在一个图中,如果任意两个顶点构成的偶对(vi, vj)∈E是有序的,即顶点之间的连线是有方向的,则称该图为有向图。在这里插入图片描述
⑶顶点、边、弧、弧头、弧尾:图中数据元素vi称为顶点;P(vi, vj)表示在顶点vi和顶点vj之间有一条直接连线。如果是在无向图中,则称这条连线为边;如果是在有向图中,一般称这条连线为弧。边用顶点的无序偶对(vi, vj)来表示,称顶点vi和顶点vj互为邻接点,边(vi, vj)依附于顶点vi与顶点vj;弧用顶点的有序偶对<vi, vj>来表示,有序偶对的第一个结点vi被称为始点(或弧尾);有序偶对的第二个结点vj被称为终点(或弧头)。

​ ⑷无向完全图:在一个无向图中,如果任意两顶点都有一条直接边相连接,则称该图为无向完全图。可以证明,在一个含有n个顶点的无向完全图中,有n(n-1)/2条边。

​ ⑸有向完全图:在一个有向图中,如果任意两顶点之间都有方向互为相反的两条弧相连接,则称该图为有向完全图。在一个含有n个顶点的有向完全图中,有n(n-1)条边。

​ ⑹稠密图、稀疏图:若一个图接近完全图,称为稠密图;称边数很少的图为稀疏图。

​ ⑺顶点的度、入度、出度:顶点的度是指依附于某顶点v的边数,通常记为TD (v)。在有向图中,要区别顶点的入度与出度的概念。顶点v的入度是指以顶点 为终点的弧的数目。记为ID (v);顶点v出度是指以顶点v为始点的弧的数目,记为OD (v)。有TD (v)=ID (v)+OD (v)。

​ 可以证明,对于具有n个顶点、e条边的图,顶点vi的度TD (vi)与顶点的个数以及边的数目满足关系: 在这里插入图片描述
⑻边的权、网图:与边有关的数据信息称为权。在实际应用中,权值可以有某种含义。比如,在一个反映城市交通线路的图中,边上的权值可以表示该条线路的长度或者等级;对于反映工程进度的图而言,边上的权值可以表示从前一个工程到后一个工程所需要的时间等等。边上带权的图称为网图或网络。如果边是有方向的带权图,就是一个有向网图。

​ ⑼路径、路径长度:顶点vp到顶点vq之间的路径是指顶点序列vp,vi1,vi2, …, vim,vq。其中,(vp,vi1),(vi1,vi2),…,(vim,vq)分别为图中的边。路径上边的数目称为路径长度。

​ ⑽回路、简单路径、简单回路:若一条路经的始点和终点是同一个点,则该路径为回路或者环;若路径中的顶点不重复出现,则该路径称为简单路径。除第一个顶点与最后一个顶点之外,其他顶点不重复出现的回路称为简单回路,或者简单环。

​ ⑾子图:对于图G=(V,E),G’=(V’,E’),若存在V’是V的子集 ,E’是E的子集 ,则称图G’是G的一个子图。下图示出了G2和G1的两个子图G’和G’’。
在这里插入图片描述
 ⑿连通的、连通图、连通分量:在无向图中,如果从一个顶点vi到另一个顶点vj(i≠j)有路径,则称顶点vi和vj是连通的。如果图中任意两顶点都是连通的,则称该图是连通图。无向图的极大连通子图称为连通分量。下图 (a)中有两个连通分量,如图 (b)所示。在这里插入图片描述
⒀强连通图、强连通分量:对于有向图来说,若图中任意一对顶点vi 和vj(i≠j)均有从一个顶点vi到另一个顶点vj有路径,也有从vj到vi的路径,则称该有向图是强连通图。有向图的极大强连通子图称为强连通分量。

​ 左下图中有两个强连通分量,分别是{v1,v2,v3}和{v4},如右下图所示。在这里插入图片描述
⒁生成树:所谓连通图G的生成树,是G的包含其全部n 个顶点的一个极小连通子图。它必定包含且仅包含G的n-1条边。下图示出了图G1 的一棵生成树。在这里插入图片描述
​ ⒂生成森林:在非连通图中,由每个连通分量都可得到一个极小连通子图,即一棵生成树。这些连通分量的生成树就组成了一个非连通图的生成森林。

8.1.2图的基本操作

⑴CreatGraph(G):输入图G的顶点和边,建立图G的存储。⑵DestroyGraph(G):释放图G占用的存储空间。

⑶GetVex(G, v):在图G中找到顶点v,并返回顶点v的相关信息。

⑷PutVex(G, v, value):在图G中找到顶点v,并将value值赋给顶点v。⑸InsertVex(G, v):在图G中增添新顶点v。

⑹DeleteVex(G, v):在图G中,删除顶点v以及所有和顶点v相关联的边或弧。

⑺InsertArc(G, v, w):在图G中增添一条从顶点v到顶点w的边或弧。⑻DeleteArc(G, v, w):在图G中删除一条从顶点v到顶点w的边或弧。⑼DFSTraverse(G, v):在图G中,从顶点v出发深度优先遍历图G。⑽BFSTtaverse(G, v):在图G中,从顶点v出发广度优先遍历图G。

​ 在图中,顶点是没有先后次序的,但当采用某一种确定的存储方式存储后,存储结构中顶点的存储次序构成了顶点之间的相对次序;同样的道理,对一个顶点的所有邻接点,采用该顶点的第i个邻接点表示与该顶点相邻接的某个顶点的存储顺序,在这种意义下,图的基本操作还有:

⑾LocateVex(G, u):在图G中找到顶点u,返回该顶点在图中位置。⑿FirstAdjVex(G, v):在图G中,返回v的第一个邻接点。若顶点在G中没有邻接顶点,则返回“空”。

⒀NextAdjVex(G, v, w):在图G中,返回v的(相对于w的) 下一个邻接顶点。若w是v的最后一个邻接点,则返回“空”。

8.2图的存储表示

8.2.1邻接矩阵

​ 所谓邻接矩阵(Adjacency Matrix)的存储结构,就是用一维数组存储图中顶点的信息,用矩阵表示图中各顶点之间的邻接关系。

​ 假设图G=(V,E)有n个确定的顶点,即V={v0,v1,…,vn-1},则表示G中各顶点相邻关系为一个n×n的矩阵,矩阵的元素为:在这里插入图片描述
若G是网图,则邻接矩阵可定义为:在这里插入图片描述
其中,wij表示边(vi,vj)或<vi,vj>上的权值;∞表示一个计算机允许的、大于所有边上权值的数。

​ 用邻接矩阵表示法表示的无向图和网图如下图所示。在这里插入图片描述
从图的邻接矩阵存储方法容易看出这种表示具有以下特点:

①无向图的邻接矩阵一定是一个对称矩阵。因此,在具体存放邻接矩阵时只需存放上(或下)三角矩阵的元素即可。

②对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度TD(vi)。

③对于有向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的出度OD(vi)(或入度ID(vi))。

④用邻接矩阵方法存储图,很容易确定图中任意两个顶点之间是否有边相连;但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。这是用邻接矩阵存储图的局限性。

​ 在用邻接矩阵存储图时,除了用一个二维数组存储用于表示顶点间相邻关系的邻接矩阵外,还需用一个一维数组来存储顶点信息,另外还有图的顶点数和边数。故可将其形式描述如下:

​ #define MaxVertexNum … //根据实际需要设定的最大顶点数

​ typedef char VertexType; //顶点类型设为字符型

​ typedef int EdgeType; //边的权值设为整型

​ typedef struct {

​ VertexType vexs[MaxVertexNum]; //顶点表

​ EdeType edges[ MaxVertexNum ]; //邻接矩阵,即边表

​ int vnum,enum; //顶点数和边数

​ }Mgragh; //Maragh是以邻接矩阵存储的图类型

//建立一个有向图的邻接矩阵存储的算法
void CreateMGraph(MGraph *G){
    //建立有向图G的邻接矩阵存储
    int i,j,k;
    char ch;
    cout<<"请输入顶点数和边数"<<endl;
    cin>>G->vnum>>G->enum;//输入定点数和边数
    cout<<"输入顶点信息"<<endl;
    for(i=0;i<G->vnum;i++)
        cin>>G->vexs[i];//输入顶点信息,建立顶点表
    for(i=0;i<G->vnum;i++)
        for(j=0;j<G->vnum;j++)
            G->edges[i][j]=0;//初始化邻接矩阵
    cout<<"请输入每条边对应的两个顶点的序号"<<endl;
    for(k=0;k<G->enum;k++)
    {
        cin>>i>>j;//依次输入e条边,每一条边用顶点的序号对偶表示
        G->edges[i][j]=1;//若为无向图,还要建立(j,i)条边,即G-》edges[j][i]=1;
    }    
}

8.2.2 邻接表

​ 邻接表(Adjacency List)是图的一种顺序存储与链式存储结合的存储方法。

​ 邻接表表示法类似于树的孩子链表表示法。就是对于图G中的每个顶点vi,将所有邻接于vi的顶点vj链成一个单链表,这个单链表就称为顶点vi的邻接表,再将所有点的邻接表表头放到数组中,就构成了图的邻接表。

​ 在邻接表表示中有两种结点结构,如下图所示。在这里插入图片描述
一种是顶点表的结点结构,它由顶点域(vertex)和指向第一条邻接边的指针域(firstedge)构成,另一种是边表(即邻接表)结点,它由邻接点域(adjvex)和指向下一条邻接边的指针域(next)构成。

​ 对于网图的边表需再增设一个存储边上信息(如权值等)的域(info),网图的边表结构如下图所示。
在这里插入图片描述
下图给出无向图及对应的邻接表表示。
在这里插入图片描述
在这里插入图片描述
邻接表表示的形式描述如下:

​ #define MaxVerNum 100 /最大顶点数为100/

typedef struct node{ /边表结点/

​ int adjvex; /邻接点域/

​ struct node * next; /指向下一个邻接点的指针域/

​ /若要表示边上信息,则应增加一个数据域info/

​ }EdgeNode;

​ typedef struct vnode{ /顶点表结点/

​ VertexType vertex; /顶点域/

​ EdgeNode * firstedge; /边表头指针/

​ }VertexNode;

​ typedef VertexNode AdjList[MaxVertexNum]; /AdjList是邻接表类型/

​ typedef struct{

​ AdjList adjlist; /邻接表/

​ int n,e; /顶点数和边数/

​ }ALGraph; /ALGraph是以邻接表方式存储的图类型/

void CreateALGraph(ALGraph *G){
    //建立有向图的邻接表存储
    int i,j,k;
    EdgeNode *s;
    cout<<"请输入顶点数和边数"<<endl;
    cin>>G->vnum>>G->enum;//读入顶点数和边数
    cout<<"请输入顶点信息"<<endl;
    for(i=0;i<G->vnum;i++)//建立有n个顶点的顶点表
    {
        cin>>G->adjlist[i].vertex;//读入顶点信息
        G->adjlist[i].firstedge=NULL;//顶点的边表头指针设为空
    }
    cout<<"请输入边的信息:"<<endl;
    for(k=0;k<G->enum;k++)//建立边表
    {
        cin>>i>>j;//读入边<vi,vj>的顶点对应序号
        s=(EdgeNode*)malloc(sizeof(EdfeNode));//生成新边表结点s
        s->adjvex=j;//邻接点序号为j
        s->next=G->AdjList[i].firstedge;//将新表结点s插入到顶点vi的边表头部
        G->AdjList[i].firstedg=s;
    }
}

​ 若无向图中有n 个顶点、e条边,则它的邻接表需n个头结点和2e个表结点。

​ 显然,在边稀疏(e<<n(n-1)/2)的情况下,用邻接表表示图比邻接矩阵节省存储空间,当和边相关的信息较多时更是如此。

​ 在无向图的邻接表中,顶点vi的度恰为第i个链表中的结点数;而在有向图中,第i个链表中的结点个数只是顶点vi的出度,为求入度,必须遍历整个邻接表。在所有链表中其邻接点域的值为i的结点的个数是顶点vi的入度。

​ 有时,为了便于确定顶点的入度或以顶点vi为头的弧,可以建立一个有向图的逆邻接表,即对每个顶点vi 建立一个链接以vi为头的弧的链表。

​ 下图所示为有向图G2的邻接表和逆邻接表。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
​ 复杂度:在建立邻接表或逆邻接表时,若输入的顶点信息即为顶点的编号,则建立邻接表的复杂度为O(n+e)。否则,需要通过查找才能得到顶点在图中位置,则时间复杂度为O(n*e)。

​ 在邻接表上容易找到任一顶点的第一个邻接点和下一个邻接点,但要判定任意两个顶点(vi 和vj)之间是否有边或弧相连,则需搜索第i个或第j个链表,因此,不及邻接矩阵方便。

8.2.3 十字链表

有向图的十字链表存储表示的形式描述如下:

#define MAX_VERTEX_NUM 20

typedef struct ArcBox {

​ int tailvex,headvex; /该弧的尾和头顶点的位置/

​ struct ArcBox * hlink, *tlink; /分别为弧头相同和弧尾相同的弧的链域/

​ InfoType info; /该弧相关信息的指针/

}ArcBox;

typedef struct VexNode {

​ VertexType vertex:

​ ArcBox fisrin, firstout; /分别指向该顶点第一条入弧和出弧/

}VexNode;

typedef struct {

​ VexNode xlist[MAX_VERTEX_NUM]; /表头向量/

​ int vexnum,arcnum; /有向图的顶点数和弧数/

​ }OLGraph;

​ 下面给出建立一个有向图的十字链表存储的算法。通过该算法,只要输入n 个顶点的信息和e条弧的信息,便可建立该有向图的十字链表。

void CreateDG(OLGraph *G){
    //采用十字链表表示,构造有向图G
    typedef char VertexType;//定点类型为字符型
    int i,j,k;
    ArcBox p;
    Vertex Type v1,v2;
    InfoType v1,v2;
    InfoType *IncInfo;
    cin>>G->vexnum>>G->arcnum>>IncInfo;//IncInfo为0则各弧不含其他信息
    for(i=0;i<G->vexum;++i)//构造表头向量
    {
        cin>>G->xlist[i].vertex;//输入顶点值
        G->xlist[i].firstin=G->xlist[i].firstout==NULL;//初始化指针
    }
    for(k=0;k<G->enum;k++)//输入各弧并构造十字链表
    {
        cin>>i>>j;//读入边的顶点对应序号
        p=(ArcBox*)malloc(sizeof(ArcBox));//申请一个弧结点
        p->tailvex=i;
        p->tlink=G->xlist[i].firstout;
        p->headvex=j;
        p->hlink=G->xlist[j].firstin;//对应节点赋值
        G->xlist[j].firstin=G->xlist[i].firstout=p;//完成在入弧和出弧链头的插入
        input(p->info);//若弧含有相关信息,则输入,否则舍去
    }
}

​ 在十字链表中既容易找到以为尾的弧,也容易找到以 vi为头的弧,因而容易求得顶点的出度和入度(或需要,可在建立十字链表的同时求出)。

​ 同时,由建立一个有向图的十字链表存储的算法可知,建立十字链表的时间复杂度和建立邻接表是相同的。

​ 在某些有向图的应用中,十字链表是很有用的工具。

8.2.4 邻接多重表

​ 邻接多重表(Adjacency Multilist)主要用于存储无向图。

​ 因为,如果用邻接表存储无向图,每条边的两个边结点分别在以该边所依附的两个顶点为头结点的链表中,这给图的某些操作带来不便。例如,对已访问过的边做标记,或者要删除图中某一条边等,都需要找到表示同一条边的两个结点。因此,在进行这一类操作的无向图的问题中采用邻接多重表作存储结构更为适宜。

​ 邻接多重表的存储结构和十字链表类似,也是由顶点表和边表组成,每一条边用一个结点表示,其顶点表结点结构和边表结点结构如下图所示。在这里插入图片描述
顶点表由两个域组成,vertex域存储和该顶点相关的信息;firstedge域指示第一条依附于该顶点的边。在这里插入图片描述
顶点表由两个域组成,vertex域存储和该顶点相关的信息firstedge域指示第一条依附于该顶点的边;边表结点由六个域组成,mark为标记域,可用以标记该条边是否被搜索过;ivex和jvex为该边依附的两个顶点在图中的位置;ilink指向下一条依附于顶点ivex的边;jlink指向下一条依附于顶点jvex的边,info为指向和边相关的各种信息的指针域。

在这里插入图片描述
例如,上图为无向图G1的邻接多重表。在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,则每个边结点同时链接在两个链表中。

​ 对无向图而言,其邻接多重表和邻接表的差别,仅仅在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。因此,除了在边结点中增加一个标志域外,邻接多重表所需的存储量和邻接表相同。在邻接多重表上,各种基本操作的实现亦和邻接表相似。

​ 邻接多重表存储表示的形式描述如下:

#define MAX_VERTEX_NUM 20

typedef emnu{ unvisited,visited} VisitIf;

typedef struct EBox{

​ VisitIf mark: /访问标记/

​ int ivex,jvex; /该边依附的两个顶点的位置/

​ struct EBox ilink, jlink; /分别指向依附这两个顶点的下一条边/

​ InfoType info; /该边信息指针/

}EBox;

typedef struct VexBox{

​ VertexType data;

EBox fistedge; /指向第一条依附该顶点的边/

}VexBox;

typedef struct{

​ VexBox adjmulist[MAX_VERTEX_NUM];

​ int vexnum,edgenum; /无向图的当前顶点数和边数/

}AMLGraph;

8.3图的遍历

​ 图的遍历是指从图中的任一顶点出发,对图中的所有顶点访问一次且只访问一次。图的遍历是图的一种基本操作,图的许多其它操作都是建立在遍历操作的基础之上。

​ 由于图结构本身的复杂性,所以图的遍历操作也较复杂,主要表现在以下四个方面:

①在图结构中,没有一个“自然”的首结点,图中任意一个顶点都可作为第一个被访问的结点。

②在非连通图中,从一个顶点出发,只能够访问它所在的连通分量上的所有顶点,因此,还需考虑如何选取下一个出发点以访问图中其余的连通分量。 ③在图结构中,如果有回路存在,那么一个顶点被访问之后,有可能沿回路又回到该顶点。

④在图结构中,一个顶点可以和其它多个顶点相连,当这样的顶点访问过后,存在如何选取下一个要访问的顶点的问题。

8.3.1 深度优先搜索

​ 深度优先搜索(Depth_Fisrst Search)遍历类似于树的先根遍历,是树的先根遍历的推广。

​ 假设初始状态是图中所有顶点未曾被访问,则深度优先搜索可从图中某个顶点发v出发,访问此顶点,然后依次从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到;若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

​ 以下图的无向图G5为例,进行图的深度优先搜索。假设从顶点v1出发进行搜索,在访问了顶点v1之后,选择邻接点v2。因为v2未曾访问,则从v2出发进行搜索。依次类推,接着从v4 、v8 、v5 出发进行搜索。在访问了v5之后,由于v5的邻接点都已被访问,则搜索回到 v8。由于同样的理由,搜索继续回到v4,v2直至v1,此时由于v1的另一个邻接点未被访问,则搜索又从v1到v3,再继续进行下去由此,得到的顶点访问序列为:

​ v1 →v2 →v4→v8→ v5 →v3→v6→v7在这里插入图片描述
显然,这是一个递归的过程。为了在遍历过程中便于区分顶点是否已被访问,需附设访问标志数组visited[0:n-1],其初值为FALSE ,一旦某个顶点被访问,则其相应的分量置为TRUE。

//深度优先遍历算法
void DFSI(Graph G,int v){
    //从第v各顶点出发递归地深度优先深度遍历图G
    int w;
    visited[v]=1;
    VisitFunc(v);//访问第v个节点
    for(w=FisrAdjVex(G,v);w;w=NextAdjVex(G,v,w))
        if(!visited[w])
            DFS(G,w);//对v的尚未访问的邻接顶点w递归调用DFS
}

以下算法给出了对以邻接表为存储结构的整个图G进行深度优先遍历实现描述。

#define MaxVerNum ...//图中最多的顶点个数
int flag[MaxVerNum]={0};
void DFSTraverse(Graph *G,int v)
{
    //从顶点i为出发点,对邻接表存储的图G进行DFS搜索
    EdgeNode *p;
    Visited(G->AdjList[i].vertex);//访问结点vi
    flag[i]=1;//标记顶点i已访问
    p=G->AdjList[i].firstedge;//取顶点i邻接表的头指针
    while(p)
    {
        j=p->adjvex;//依次搜索顶点i的邻接点顶点j
        if(!flag[j])//若顶点j尚未访问,则以顶点j为出发点深度搜索
            DFSAL(G,j);
        p=p->next;//找顶点i的下一个邻接点
    }
}
void DFSTraverseAL(ALGraph *G)
{
    //对邻接表存储的图G进行深度优先遍历
    int i;
    for(i=0;i<G->vnum;i++)
        if(!flag[i]) DFS(G,i);//若vi未访问过,从vi开始DFS搜
}
    

分析上述算法,在遍历时,对图中每个顶点至多调用一次DFS 函数,因为一旦某个顶点被标志成已被访问,就不再从它出发进行搜索。

​ 因此,遍历图的过程实质上是对每个顶点查找其邻接点的过程。其耗费的时间则取决于所采用的存储结构:①当用二维数组表示邻接矩阵图的存储结构时,查找每个顶点的邻接点所需时间为O(n2) ,其中n为图中顶点数。②而当以邻接表作图的存储结构时,找邻接点所需时间为O(e),其中e为无向图中边的数或有向图中弧的数。

​ 由此,当以邻接表作存储结构时,深度优先搜索遍历图的时间复杂度为O(n+e) 。

8.3.2 广度优先搜索

​ 广度优先搜索(Breadth_First Search) 遍历类似于树的按层次遍历的过程。

​ 假设从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过和邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

​ 换句话说,广度优先搜索遍历图的过程中以v为起始点,由近至远,依次访问和v有路径相通且路径长度为1,2,…的顶点。

​ 例如,对下图所示无向图G5 进行广度优先搜索遍历,首先访问v1 和v1的邻接点v2和v3,然后依次访问v2 的邻接点v4 和v5 及v3 的邻接点v6和v7,最后访问v4 的邻接点v8。由于这些顶点的邻接点均已被访问,并且图中所有顶点都被访问,由些完成了图的遍历。得到的顶点访问序列为: v1→v2 →v3 →v4→ v5→ v6→ v7 →v8在这里插入图片描述
和深度优先搜索类似,在遍历的过程中也需要一个访问标志数组。并且,为了顺次访问路径长度为2、3、…的顶点,需附设队列以存储已被访问的路径长度为1、2、… 的顶点。从图的某一点v出发,递归地进行广度优先遍历的过程如算法所示。

//图的广度优先遍历
int flag[N]={0};//全局数组初始化为0
void BFSTraverse(Graph *G,int v)
{
    //以顶点v为出发点广度优先遍历图G
    InitQueue(Q);//置空队列Q
    if(!flag[v])//v尚未访问
    {
        EnQueue(Q,v);//v入队
        flag[v]=1;//置访问标记
        while(!QueueEmpty(Q))//队列不为空
        {
            DeQueue(Q,u);//队头元素出队
            Visited(u);//访问该结点
            for(w=FirstAdjVex(G,u);w;w=NextAdjVex(G,u,w))
                if(!flag[w])//依次将尚未访问的邻接顶点入队列,置访问标记
                {
                    EnQueue(Q,w);
                    flag[w]=1;
                }
        }
    }
}

以下算法给出了对以邻接矩阵为存储结构的整个图G进行深度优先遍历。

//广度优先遍历邻接矩阵存储的图
#define MaxVerNum ...//图中最多的顶点个数
int flag[MaxVerNum]={0};
void BFSM(MGraph *G,int i)
{
    //以顶点i为出发点,对邻接矩阵存储的图G进行BFS搜索
    int j;
    int Q[MaxVerNum],front,rear;//定义队列空间和队头队尾变量
    front=0;rear=-1;//初始化为空
    Visited(G->vexs[i]);//访问顶点i
    flag[i]=1;//置访问标志
    rear++;Q[rear]=i;//出发点顶点i入队
    while(front<=rear)//当队不为空
    {
        i=Q[front];front++;//出队
        for(j=0;j<G->vnum;j++)//依次搜索顶点i的邻接点
            if(G->edges[i][j]==1&&!flag[j])//若邻接点顶点j未访问
            {
                Visited(G->vexs[j]);//访问顶点j
                flag[j]=1;//置访问标志
                rear++;
                Q[rear]=j;//访问过的顶点j入队
            }
    }
}
void BFSTraverseAL(MGraph *G)//广度优先遍历以邻接矩阵存储的图G
{
    //图G以邻接矩阵存储,N是顶点个数,对G进行广度优先遍历
    int i;
    for(i=0;i<G->vnum;i++)
        if(!flag[i]) BFS(G,i);//若顶点i未访问,从顶点i开始BFS搜索
}

​ 分析上述算法,每个顶点至多进一次队列。遍历图的过程实质是通过边或弧找邻接点的过程,因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两者不同之处仅仅在于对顶点访问的顺序不同。

8.4 图的连通性

8.4.1 无向图的连通性

​ 在对无向图进行遍历时,对于连通图,仅需从图中任一顶点出发,进行深度优先搜索或广度优先搜索,便可访问到图中所有顶点。

​ 对非连通图,则需从多个顶点出发进行搜索,而每一次从一个新的起始点出发进行搜索过程中得到的顶点访问序列恰为其各个连通分量中的顶点集。例如,一个非连通图G3,按照G3的邻接表进行深度优先搜索遍历,需由算法调用两次DFS(即分别从顶点A 和D出发),得到的顶点访问序列分别为:

​ A B F E DC

​ 这两个顶点集分别加上所有依附于这些顶点的边,便构成了非连通图G3的两个连通分量。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述​ 因此,要想判定一个无向图是否为连通图,或有几个连通分量,就可设一个计数变量count,初始时取值为0,在算法8.5的第二个for循环中,每调用一次DFS,就给count增1。这样,当整个算法结束时,依据count的值,就可确定图的连通性了。

8.4.2 有向图的连通性

​ 有向图的连通性不同于无向图的连通性,可分为弱连通、单侧连通和强连通。对有向图的强连通性以及强连通分量的判定,可通过对以十字链表为存储结构的有向图的深度优先搜索实现。

​ 由于强连通分量中的结点相互可达,故可先按出度深度优先搜索,记录下访问结点的顺序和连通子集的划分,再按入度深度优先搜索,对前一步的结果再划分,最终得到各强连通分量。若所有结点在同一个强连通分量中,则该图为强连通图。

​ 有向图的连通性不同于无向图的连通性,可分为弱连通、单侧连通和强连通。这里仅就有向图的强连通性以及强连通分量的判定进行介绍。

​ 深度优先搜索是求有向图的强连通分量的一个有效方法。假设以十字链表作有向图的存储结构,则求强连通分量的步骤如下:

​ ⑴在有向图G上,从某个顶点出发沿以该顶点为尾的弧进行深度优先搜索遍历,并按其所有邻接点的搜索都完成(即退出DFS函数)的顺序将顶点排列起来。此时需对8.3.1 中的算法作如下两点修改:

①在进入DFSTraverseAL 函数时首先进行计数变量的初始化,即在入口处加上count=0的语句;

②在退出函数之前将完成搜索的顶点号记录在另一个辅助数组finished[vexnum]中,即在函数DFSAL结束之前加上finished[++count]=v 的语句。

⑵在有向图G上,从最后完成搜索的顶点(即finished[vexnum -1]中的顶点)出发,沿着以该顶点为头的弧作逆向的深度搜索遍历,若此次遍历不能访问到有向图中所有顶点,则从余下顶点中最后完成搜索的那个顶点出发,继续作逆向的深度优先搜索遍历。依次类推,直至有向图中所有顶点都被访问到为止。此时调用DFSTraverseAL 时需作如下修改:函数中第二个循环语句的边界条件应改为v从finished[vexnum-1]至finished[0]。

​ 由此,每一次调用DFSAL作逆向深度优先遍历所访问到的顶点集便是有向图G中一个强连通分量的顶点集。

​ 例如下图所示的有向图,假设从顶点v1出发作深度优先搜索遍历,得到finished数组中的顶点号为(1,3,2,0) ;则再从顶点v1出发作逆向的深度优先搜索遍历,得到两个顶点集{ v1, v3, v4}和{ v2},这就是该有向图的两个强连通分量的顶点集。
在这里插入图片描述
在这里插入图片描述
​ 上述求强连通分量的第二步,其实质为:

​ ⑴构造一个有向图Gr,设G=(V,{A}),则Gr=(Vr,{Ar})对于所有< vi,vj>∈A,必有< vj, vi >∈Ar。即Gr中拥有和G 方向相反的弧;

​ ⑵在有向图Gr上,从顶点finished[vexnum-1] 出发作深度优先遍历。可以证明,在Gr上所得深度优先生成森林中每一棵树的顶点集即为G的强连通分量的顶点集。

​ 显然,利用遍历求强连通分量的时间复杂度亦和遍历相同。

​ 这一节将给出通过对图的遍历,得到图的生成树或生成森林的算法。

​ 设E(G)为连通图G中所有边的集合,则从图中任一顶点出发遍历图时,必定将E(G)分成两个集合T(G)和B(G),其中,T(G)是遍历图过程中历经的边的集合;B(G)是剩余的边的集合。

​ 显然,T(G)和图G中所有顶点一起构成连通图G的极小连通子图。它是连通图的一棵生成树,并且由深度优先搜索得到的为深度优先生成树;由广度优先搜索得到的为广度优先生成树。

8.4.3生成树和生成森林

例如,图(a)和(b)所示分别为连通图G5的深度优先生成树和广度优先生成树。图中虚线为集合B(G) 中的边,实线为集合T(G)中的边。在这里插入图片描述
在这里插入图片描述在这里插入图片描述
对于非连通图,通过遍历将得到的是生成森林。例如,下图所示为一个无向图及其深度优先生成森林,它由三棵深度优先生成树组成。

在这里插入图片描述

假设以孩子兄弟链表作生成森林的存储结构,下面算法生成非连通图的深度优先生成森林。

//非连通图的深度优先生成森林算法
void DFSTree(Graph *G,int v,CSTree *T){
    //从第v个顶点出发深度优先遍历图G,建立以T为根的生成树
    int w,first;
    CSTNode *p,*q;
    visited[v]=1;
    first=1;//first 用于标记是否访问了第一个孩子
    for(w=FirstAdjVex(G,v);w;w=NextAdjVex(G,v,w))
        if(!visited[w])
        {
            p=newCSNode;//分配孩子节点
            *p={GetVex(G,w),NULL,NULL};
            if(first)//w是v的第一个未被访问的邻接顶点,作为根的左孩子节点
            {
                *T->lchild=p;
                first=0;
            }
            else q->nextsibling=p;//w是v的其他未被访问的邻接顶点,作为上衣邻接点的右兄弟
            q=p;
            DFSTree(G,w,&q);//从第w个顶点出发深度优先遍历图G,建立以q为根的生成子树
        }
}

8.4.4 关节点和重连通分量

假若在删去顶点v以及和v相关联的各边之后,将图的一个连通分量分割成两个或两个以上的连通分量,则称顶点v为该图的一个关节点。一个没有关节点的连通图称为重连通图。在重连通图上,任意一对顶点之间至少存在两条路径,则在删去某个顶点以及依附于该顶点的各边时也不破坏图的连通性。若在连通图上至少删去k个顶点才能破坏图的连通性,则称此图的连通度为 k。关节点和重连通图在实际中较多应用。显然,一个表示通信网络的图的连通度越高,其系统越可靠,无论是哪一个站点出现故障或遭到外界破坏,都不影响系统的正常工作;又如,一个航空网若是重连通的,则当某条航线因天气等某种原因关闭时,旅客仍可从别的航线绕道而行;再如,若将大规模的集成电路的关键线路设计成重连通的话,则在某些元件失效的情况下,整个片子的功能不受影响,反之,在战争中,若要摧毁敌方的运输线,仅需破坏其运输网中的关节点即可。

例如,图 (a)中图G7是连通图,但不是重连通图。图中有四个关节点A、B 和G 。若删去顶点B以及所有依附顶点B的边,G7 就被分割成三个连通分量{A、C、F、L、M、J}、{G、H、I、K}和{D、E}。类似地,若删去顶点A或G以及所依附于它们的边,则G7被分割成两个连通分量,由此,关节点亦称为割点。在这里插入图片描述
利用深度优先搜索便可求得图的关节点,并由此可判别图是否是重连通的。

​ 上图 (b)所示为从顶点A出发深优先生成树,图中实线表示树边,虚线表示回边(即不在生成树上的边)。对树中任一顶点v而言,其孩子结点为在它之后搜索到的邻接点,而其双亲结点和由回边连接的祖先结点是在它之前搜索到的邻接点。由深度优先生成树可得出两类关节点的特性:

​ ⑴若生成树的根有两棵或两棵以上的子树,则此根顶点必为关节点。因为图中不存在连接不同子树中顶点的边,因此,若删去根顶点,生成树便变成生成森林。

​ ⑵若生成树中某个非叶子顶点v,其某棵子树的根和子树中的其它结点均没有指向v的祖先的回边,则v为关节点。因为,若删去v,则其子树和图的其它部分被分割开来。

若对图Graph=(V,{Edge}) 重新定义遍历时的访问函数visited,并引入一个新的函数low,则由一次深度优先遍历便可求得连通图中存在的所有关节点。

定义visited[v]为深度优先搜索遍历连通图时访问顶点 v的次序号;定义:

low(v) = Min (visited[v],low[w],visited[k] )

w是v在DFS生成树上的孩子结点;

k是v在DFS生成树上由回边联结的祖先结点;

(v,w)∈Edge; (v,k)∈Edge,

若对于某个顶点v,存在孩子结点w且low[w]≧visited[v],则该顶点v必为关节点。因为当w是v的孩子结点时,low[w]≧visited[v],表明w 及其子孙均无指向v的祖先的回边。

​ 由定义可知,visited[v]值即为v在深度优先生成树的前序序列的序号,只需将DFS函数中头两个语句改为visited[v0]=++count(在DFSTraverse中设初值count=1)即可;low[v]可由后序遍历深度优先生成树求得,而v在后序序列中的次序和遍历时退出DFS函数的次序相同,由此修改深度优先搜索遍历的算法便可得到求关节点的算法。

8.5 最小生成树

8.5.1 最小生成树的基本概念

​ 由生成树的定义可知,无向连通图的生成树不是唯一的。连通图的一次遍历所经过的向前边的集合及图中所有顶点的集合就构成了该图的一棵生成树,对连通图的不同遍历,如遍历出发点不同或存储点的顺序不同,就可能得到不同的生成树。下图 (a)、(b)和©所示的均为无向连通图G5的生成树。在这里插入图片描述
可以证明,对于有n个顶点的无向连通图,无论其生成树的形态如何,所有生成树中都有且仅有n-1条边。

​ 如果无向连通图是一个网,那么,它的所有生成树中必有一棵边的权值总和最小的生成树,我们称这棵生成树为最小生成树,简称最小生成树。

​ 最小生成树的概念可以应用到许多实际问题中。

​ 最小生成树的构造算法可依据最小生成树的MST性质得到。

​ MST性质:设G=(V,E)是一个连通网络,U是顶点集V的一个真子集。若(u,v)是G中所有的一个端点在U(即u∈U)里、另一个端点不在U(即v∈V-U)里的边中,具有最小权值的一条边,则一定存在G的一棵最小生成树包括此边(u,v)。

8.5.2 构造最小生成树的Prim算法

​ 假设G=(V,E)为一网图,其中V为网图中所有顶点的集合,E为网图中所有带权边的集合。设置两个新的集合U和T,其中集合U用于存放G的最小生成树中的顶点,集合T存放G的最小生成树中的边。令集合U的初值为U={u1}(假设构造最小生成树时,从顶点u1出发),集合T的初值为T={}。

​ Prim算法的思想:从所有u∈U,v∈V-U的边中,选取具有最小权值的边(u,v),将顶点v加入集合U中,将边(u,v)加入集合T中,如此不断重复,直到U=V时,最小生成树构造完毕,这时集合T中包含了最小生成树的所有边。

​ Prim算法可用下述过程描述,其中用wuv表示顶点u与顶点v边上的权值。

​ ⑴ U={u1},T={};

​ ⑵ while (U≠V)do

​ (u,v)=min{wuv;u∈U,v∈V-U }

​ T=T+{(u,v)}

​ U=U+{v}

​ ⑶ 结束。

​ 下图(a)所示的一个网图,按照Prim方法,从顶点1出发,该网的最小生成树的产生过程如图 (b)、©、(d)、(e)、(f)和(g)所示。在这里插入图片描述
​ 为实现Prim算法,需设置两个辅助一维数组lowcost和closevertex。其中,lowcost用来保存集合V-U中各顶点与集合U中各顶点构成的边中具有最小权值的边的权值;数组closevertex用来保存依附于该边的在集合U中的顶点。

​ 假设初始状态时,U={u1}(u1为出发的顶点),这时有lowcost[0]=0,它表示顶点u1已加入集合U中,数组lowcost的其它各分量的值是顶点u1到其余各顶点所构成的直接边的权值。

​ 然后,不断选取权值最小的边(ui,uk)(ui∈U,uk∈V-U),每选取一条边,就将lowcost(k)置为0,表示顶点uk已加入集合U中。由于顶点uk从集合V-U进入集合U后,这两个集合的内容发生了变化,就需依据具体情况更新数组lowcost和closevertex中部分分量的内容。最后closevertex中即为所建立的最小生成树。

//采用邻接矩阵存储的Prim算法
#define MaxVerNum... //定义能存储的足够大的顶点个数
#define MAXCOST...//定义MAXCOST为一个足够大的常值量
void Prim(MGraph *G,int tree[MaxVerNum],int cost[MaxVerNum]){
    //从v0的顶点出发,建立连通网的最小生成树
    //建立的最小生成树存于数组tree中,对应的边值再cost中
    int flag[MaxVerNum]={0};
    int i,j,k,mincost;
    for(i=1;i<G->vnum;i++)
    {
        cost[i]=G->edges[0][i];//从存储序号为0的v0顶点出发生成最小生成树
        tree[i]=0;
    }
    flag[0]=1;//v0进入U集合
    tree[0]=1;//v0最小生成树的根结点,没有双亲
    for(i=1;i<G->vnum;i++)
        //寻找当前最小权值的边
    {
        mincost=MAXCOST;
        for(j=1;j<G->vnum;j++)
        {
            if(flag[j]==o&&cost[j]<mincost)
            {
                mincost=cost[j];
                k=j;//记忆最小的边
            }
        }
        flag[k]=1;//k进入了U集合
        for(j=1;j<G->vnum;j++)//是否用新点k连通不在U的顶点
            if(flag[j]==0&&G->edges[k][j]<cost[j])
            {
                cost[j]=G->edges[k][j];
                tree[j]=k;
            }
    }
}

​ 在Prim算法中,第一个for循环的执行次数为n-1,第二个for循环中又包括了一个while循环和一个for循环,执行次数为2(n-1)^2,所以Prim算法的时间复杂度为O(n2)。

​ 下图给出了在用上述算法构造下图无向网的最小生成树的过程中,数组closevertex、lowcost及集合U,V-U的变化情况,可进一步加深对Prim算法的了解。
在这里插入图片描述
在这里插入图片描述

8.5.3 构造最小生成树的Kruskal算法

​ Kruskal算法是一种按照网中边的权值递增的顺序构造最小生成树的方法。

​ 基本思想:设无向连通网为G=(V,E),令G的最小生成树为T,其初态为T=(V,{}),即开始时,最小生成树T由图G中的n个顶点构成,顶点之间没有一条边,这样T中各顶点各自构成一个连通分量。然后,按照边的权值由小到大的顺序,考察G的边集E中的各条边。若被考察的边的两个顶点属于T的两个不同的连通分量,则将此边作为最小生成树的边加入到T中,同时把两个连通分量连接为一个连通分量;若被考察边的两个顶点属于同一个连通分量,则舍去此边,以免造成回路,如此下去,当T中的连通分量个数为1时,此连通分量便为G的一棵最小生成树。

​ 对于前图所示的网,按照Kruskal方法构造最小生成树的过程如下图所示。在构造过程中,按照网中边的权值由小到大的顺序,不断选取当前未被选取的边集中权值最小的边。依据生成树的概念,n个结点的生成树,有n-1条边,故反复上述过程,直到选取了n-1条边为止,就构成了一棵最小生成树。在这里插入图片描述
在这里插入图片描述
为了实现Kruskal算法,需设置一个结构数组Edges存储网中所有的边,边的结构类型包括构成的顶点信息和边权值,定义如下:

​ #define MAXEDGE <图中的最大边数>

​ typedef struct {

​ elemtype v1;

​ elemtype v2;

​ int cost;

} EdgeType;

EdgeType edges[MAXEDGE]

​ 在结构数组edges中,每个分量edges[i]代表网中的一条边,其中edges[i].v1和edges[i].v2表示该边的两个顶点,edges[i].cost表示这条边的权值。

​ 为了方便选取当前权值最小的边,事先把数组edges中的各元素按照其cost域值由小到大的顺序排列。

​ 在对连通分量合并时,采用8.5.2节所介绍的集合的合并方法。对于有n个顶点的网,设置一个数组father[n],其初值为father[i]=-1(i=0,1,…,n-1),表示各个顶点在不同的连通分量上。然后,依次取出edges数组中的每条边的两个顶点,查找它们所属的连通分量,假设vf1和vf2为两顶点所在的树的根结点在father数组中的序号,若vf1不等于vf2,表明这条边的两个顶点不属于同一分量,则将这条边作为最小生成树的边输出,并合并它们所属的两个连通分量。

​ 下面用C语言实现Kruskal算法,其中函数Find的作用是寻找图中顶点所在树的根结点在数组father中的序号。需说明的是,在程序中将顶点的数据类型定义成整型,而在实际应用中,可依据实际需要来设定。

typedef int elemtype;

​ typedef struct {

​ elemtype v1;

​ elemtype v2;

​ int cost;

​ }EdgeType;

​ EdgeType edges[MAXEDGE]

//Kruskal算法
int Find(int father[],int v){
    //寻找顶点v所在树的根结点
    int t;
    t=v;
    while(father[t]>=0)
        t=father[t];
    return t;
}
void Kruskal(EdgeType edges[],int n){
    //用Kruskal方法构造有n个顶点的图edges的最小生成树
    //edge[]中的数据已按cost值由小到大排序,最小生成树的边在T中
    int father[MaxVerNum];
    int i,j,vf1,vf2;
    for(i=0;i<G->vnum;i++)father[i]=-1;
    i=0;j=0;
    while(i<G->enum&&j<G->vnum-1)
    {
        vf1=Find(father,edges[i].v1);
        vf2=Find(father,edges[i].v2);
        if(vf1!=vf2)
        {
            father[vf2]=vf1;
            T[j]=edges[i];
            j++;
        }
        i++;
    }
}

​ 在Kruskal算法中,第二个while循环是影响时间效率的主要操作,其循环次数最多为MAXEDGE次数,其内部调用的Find函数的内部循环次数最多为n,所以Kruskal算法的时间复杂度为O(n·MAXEDGE)。

8.6 最短路径

最短路径问题:如果从图中某一顶点(称为源点)到达另一顶点(称为终点)的路径可能不止一条,如何找到一条路径使得沿此路径上各边上的权值总和达到最小。

问题解法

单源最短路径问题

​ — Dijkstra算法

所有顶点之间的最短路径

​ — Floyd算法

8.6.1 从一个源点到其他各点的最短路径

​ 最短路径问题是图的又一个比较典型的应用问题。例如,某一地区的一个公路网,给定了该网内的n个城市以及这些城市之间的相通公路的距离,能否找到城市A到城市B之间一条举例最近的通路呢?如果将城市用点表示,城市间的公路用边表示,公路的长度作为边的权值,那么,这个问题就可归结为在网图中,求点A到点B的所有路径中,边的权值之和最短的那一条路径。这条路径就是两点之间的最短路径,并称路径上的第一个顶点为源点,最后一个顶点为终点。

​ 在非网图中,最短路径是指两点之间经历的边数最少的路径。

​ 讨论单源点的最短路径问题:给定带权有向图G=(V,E)和源点v∈V,求从v 到G中其余各顶点的最短路径。

​ 解决这一问题的算法。即由迪杰斯特拉(Dijkstra)提出的一个按路径长度递增的次序产生最短路径的算法。

算法的基本思想:

​ 设置两个顶点的集合S和T=V-S,集合S中存放已找到最短路径的顶点,集合T存放当前还未找到最短路径的顶点。

​ 初始状态时,集合S中只包含源点v0,然后不断从集合T中选取到顶点v0路径长度最短的顶点u加入到集合S中。

​ 集合S每加入一个新的顶点u,都要修改顶点v0到集合T中剩余顶点的最短路径长度值,集合T中各顶点新的最短路径长度值为原来的最短路径长度值与顶点u的最短路径长度值加上u到该顶点的路径长度值中的较小值。

​ 重复此过程,直到集合T的顶点全部加入到S中为止。

​ Dijkstra算法的正确性可以用反证法加以证明。

​ 假设下一条最短路径的终点为x,那么,该路径必然或者是弧(v0,x),或者是中间只经过集合S中的顶点而到达顶点x的路径。因为假若此路径上除x之外有一个或一个以上的顶点不在集合S中,那么必然存在另外的终点不在S中而路径长度比此路径还短的路径,这与我们按路径长度递增的顺序产生最短路径的前提相矛盾,所以此假设不成立。

Dijkstra算法的实现:

​ 首先,引进一个辅助向量D,它的每个分量D[i]表示当前所找到的从始点v到每个终点vi的最短路径的长度。它的初态为:若从v到vi有弧,则D[i]为弧上的权值;否则置 D[i]为∞。

​ 显然,长度为:D[j]=Min{D[i]| vi∈V}的路径就是从v出发的第一条长度最短的最短路径。此路径为(v ,vj)。 那么,下一条长度次短的最短是哪一条呢?假设该次短路径的终点是vk,则可想而知,这条路径或者是(v, vk),或者是(v, vj, vk)。它的长度或者是从v到vk的弧上的权值,或者是D[j]和从vj到vk的弧上的权值之和。

​ 依据前面介绍的算法思想,在一般情况下,下一条长度次短的最短路径的长度必是:

​ D[j]=Min{D[i]| vi∈V-S}

​ 其中,D[i] 或者弧(v, vi)上的权值,或者是D[k]( vk∈S和弧(vk, vi)上的权值之和。

在这里插入图片描述根据以上分析,可以得到如下描述的算法:

⑴假设用带权的邻接矩阵edges来表示带权有向图,edges[i] [j]表示弧〈vi, vj〉上的权值。若〈vi, vj〉不存在,则置edges[i] [j]为∞。S为已找到从v出发的最短路径的终点的集合,它的初始状态为空集。那么,从v出发到图上其余各顶点(终点)vi可能达到最短路径长度的初值为: D[i]= edges[Locate Vex(G,v)] [i] vi∈V

​ ⑵选择vj,使得D[j]=Min{D[i]| vi∈V-S},vj就是当前求得的一条从v出发的最短路径的终点。令S=S∪{j}

​ ⑶修改从v出发到集合V-S上任一顶点vk可达的最短路径长度。如果

​ D[j]+ edges[j] [k]<D[k],则修改D[k]为: D[k]=D[j ]+ edges [ j ] [ k ]

​ 重复操作⑵、⑶共n-1次。由此求得从v到图上其余各顶点的最短路径是依路径长度递增的序列。

#define MaxVerNum...
void ShortestPath(MGraph *G,int P[MaxVerNum],float D[MaxVerNum])
{
    //设源点为顶点0,求到其余各顶点的最短路径
    //D[v]存放从源点顶点0到终点v最短路径其长度
    //P[V]存放相应的最短路径终点的前驱点
    //常量INFINITY是为邻接矩阵中的正无穷
    int final[MaxVerNum]={1,0};
    for(i=0;i<G->vnum;i++)
    {
        D[i]=G->edges[0][i];
        P[i]=0;
    }
    D[0]=0;final[0]=1;p[0]=-1;//初始化,源点属于S集
    for(i=1;i<G->vnum;i++)
    {
        //开始主循环,每次求得源点到某个顶点k的最短路径,并将k加到S集
        min=INFINITY+1;//为了将没有路径的点最后选中,初始化正无穷+1
        for(k=0;k<G->vnum;j++)//从未进入S的点中找最小的D[k]
            if(final[k]==0&&D[k]<min)//顶点K没有进入S中且当前的路径更短
            {
                j=k;//具有更小路径的点存在j中
                min=D[k];
            }
        final[j]=1;//将j加入S集合
        for(k=0;k<G->vnum;k++)//更新其他没进入S的点的当前最短路径及长度
        {
        if(final[k]==0&&(D[j]+G->edges[j][k]<D[k]))//对k∈V-S的点
        	{
            D[k]=D[j]+G->edges[j][k];//将D[k]修改为更短路径长度
            P[k]=j;//记忆对应的路径,将k的前驱结点改为j
        	}
        }
        for(i=1;i<G->vnum;i++)//输出各最短路径的长度及路径上的结点
        {
            printf("%f: %d",D[i],i);
            pre=P[i];
            while(pre>=0)
            {
                printf("⬅%d",pre);
                pre=P[pre];
            }
            printf("\n");
        }
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Dijkstra逐步求解的过程

​ 如何从表中读取源点0到终点v的最短路径?

​ 以顶点5为例:

P[5] = 3 →P[3] = 4 →P[4] = 0,反过来排列,得到路径0, 4, 3, 5,这就是源点0到终点4的最短路径。
在这里插入图片描述

8.6.2 每一对顶点之间的最短路径 —弗洛伊德算法

​ 接下来,如何求解每一对顶点之间的最短路径?

​ 解决这个问题的一个办法是:每次以一个顶点为源点,重复调用Dijkstra算法n次。这样,便可求得每一对顶点之间的最短路径,总的时间复杂度为O(n^3)。

​ 弗洛伊德(Floyd)提出了另一个算法。Floyd算法求解每一对顶点对间的最短路径问题仍从图的带权邻接矩阵出发,依据的是以下递推关系:

​ D^(-1) [i] [j]=edges[i] [j]

​ D^(k) [i] [j]=Min{D(k-1)[i] [j], D^(k-1) [ i] [k]+ D^(k-1) [ k] [j]} 0≤k≤n-1

​ 其中,二维数组edges存放的是带权图的邻接矩阵的值,D^(k) [i] [j]是从vi到vj的中间顶点的个数不大于k的最短路径的长度。因此,D^(n-1) [i] [j]是从vi到vj最短路径的长度。

​ 通过Floyd计算图G=(V,E)中各个顶点的最短路径时,需要引入一个矩阵S,矩阵S中的元素S[i] [j]表示顶点i(第i个顶点)到顶点j(第j个顶点)的距离。

​ 假设图G中顶点个数为N,则需要对矩阵S进行N次更新。初始时,矩阵S中顶点S[i] [j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则S[i] [j]=∞。

​ 接下来,对矩阵S进行N次更新。第1次更新时,如果S[i] [j]> S[i] [0]+S[0] [j] (表示i与j之间经过第1个顶点的距离),则更新S[i] [j]为S[i] [0]+S[0] [j]。 同理,第k次更新时,如果[i] [j]>S[i] [k]+S[k] [j],则更新S[i] [j]为S[i] [k]+S[k] [j]。更新N次之后,操作完成。

Floyd算法描述

1) S是记录各个顶点间最短路径的矩阵。S[i] [j]记录顶点vi到vj的最短路径长度;P[i] [j]记录顶点vi到vj的最短路径中,vj的前驱顶点对应的数组下标值。

2)初始化S。矩阵S中顶点S[i] [j]的距离为顶点i到顶点j的权值;如果i和j不相邻,则S[i] [j]=∞。即S矩阵中最初值为图G的邻接矩阵。

3)初始化P,P[i] [j]=j,表示最短路径为 (vi,vj);k=0;

4)若k>n-1,执行6);否则以顶点vk为中介点,若S[i] [j]>S[i] [k]+S[k] [j],则设置S[i] [j]=S[i] [k]+S[k] [j],P[i] [j]=k 。

5)k++; 执行4)。

6)结束。

Floyd过程描述

第一步:初始化S矩阵:在这里插入图片描述
第二步:以顶点A为中介点更新S矩阵:

在这里插入图片描述
第三步:以顶点B为中介点更新S矩阵:在这里插入图片描述
第四步:以顶点C为中介点更新S矩阵:在这里插入图片描述
第五步:以顶点D为中介点更新S矩阵:在这里插入图片描述
第六步:以顶点E为中介点更新S矩阵:在这里插入图片描述
第七步:以顶点F为中介点更新S矩阵:在这里插入图片描述第八步:以顶点G为中介点更新S矩阵:在这里插入图片描述

//求图上任意两点之间的最短路径——Floyd算法
#define MaxVerNum ...
void floyd(MGraph *G,int path[][MaxVerNum],int dist[][MaxVerNum])
{
    //path存路径。path[i][j]=k表示,“顶点i”到“顶点j”的最短路径会经过顶点k
    //dist长度数组。dist[i][j]=sum表示,“顶点i”到“顶点j”的最短路径长度是sum
    int i,j,k;
    int tmp;
    //初始化
    for(i=0;i<G->vnum;i++)
    {
        for(j=0;j<G->vnum;j++)
        {
            dist[i][j]=G->edges[i][j];//顶点i到顶点j的路径长度为i到j的权值
            path[i][j]=j;//顶点i到顶点j的最短路径是经过顶点j
        }
    }
    //计算最短路径
    for(k=0;k<G->vexnum;k++)
    {
        for(i=0;i<G->vexnum;i++)
        {
            for(j=0;j<G->vexnum;j++)
            {
                //如果经过下标为k顶点路径比原两点之间的路径更短,则更新dist[i][j]和path[i][j]
                tmp=(dist[i][k]==INF||dist[k][j]==INF)?INF:(dist[i][k]+dist[k][j]);
                if(dist[i][j]>tmp)
                {
                    //i到j最短路径,对应的值设为更小的一个(即经过k)
                    dist[i][j]=tmp;
                    //i到j最短路径对应的路径,经过k
                    path[i][j]=path[i][k];
                }
            }
        }
    }
    //打印floyd最短路径的结果
    printf("floyd:\n");
    for(i=0;i<G->vnum;i++)
    {
        for(j=0;j<G->vnum;j++)
        {
            printf("v%d到v%d的最短路径长度: %2d\n",i,j,dist[i][j]);
            printf("路径为: v%d",j);
            if(path[i][j]==j) printf("➡%d\n",i);
            else{
                k=j;
                while(path[i][j]!=k)
                {
                    k=path[i][k];
                    printf("➡v%d",k);
                }
                printf("➡v%d\n",i);
            }
        }
        printf("\n");
    }
}

8.7 有向无环图及其应用

8.8.1 有向无环图的概念

​ 一个无环的有向图称做有向无环图(directed acycline praph)。简称DAG图。DAG图是一类较有向树更一般的特殊有向图, 下图给出了有向树、 DAG图和有向图的例子。

在这里插入图片描述
有向无环图是描述含有公共子式的表达式的有效工具。例如下述表达式:

​ ((a+b)* (b* (c+d)+(c+d)* e) * ((c+d) e) 可以用第六章讨论的二叉树来表示,如下图所示。
在这里插入图片描述
仔细观察该表达式 ((a+b)
(b* (c+d)+(c+d)* e)* ((c+d)* e),可发现有一些相同的子表达式,如(c+d)和(c+d)*e等,在二叉树中,它们也重复出现。

​ 若利用有向无环图,则可实现对相同子式的共享,从而节省存储空间。例如下图所示为表示同一表达式的有向无环图。
在这里插入图片描述
检查一个有向图是否存在环要比无向图复杂。

​ 对于无向图来说,若深度优先遍历过程中遇到回边(即指向已访问过的顶点的边),则必定存在环;而对于有向图来说,这条回边有可能是指向深度优先生成森林中另一棵生成树上顶点的弧。但是,若从有向图上某个顶点v出发的遍历,在dfs(v)结束之前出现一条从顶点u到顶点v的回边,由于u 在生成树上是v的子孙,则有向图必定存在包含顶点v和u的环。

​ 有向无环图是描述一项工程或系统的进行过程的有效工具。除最简单的情况之外,几乎所有的工程(project)都可分为若干个称作活动(activity)的子工程,而这些子工程之间,通常受着一定条件的约束,如其中某些子工程的开始必须在另一些子工程完成之后。对整个工程和系统,人们关心的是两个方面的问题:一是工程能否顺利进行:二是估算整个工程完成所必须的最短时间。

8.8.2 AOV网与拓扑排序

1.AOV网(Activity on vertex network)

​ 所有的工程或者某种流程可以分为若干个小的工程或阶段,这些小的工程或阶段就称为活动。若以图中的顶点来表示活动,有向边表示活动之间的优先关系,则这样活动在顶点上的有向图称为AOV网。

​ 在AOV网中,若从顶点i到顶点j之间存在一条有向路径,称顶点i是顶点j的前驱,或者称顶点j是顶点i的后继。若<i,j>是图中的弧,则称顶点i是顶点j的直接前驱,顶点j是顶点i的直接后驱。

​ AOV网中的弧表示了活动之间存在的制约关系。例如,计算机专业的学生必须完成一系列规定的基础课和专业课才能毕业。在这里插入图片描述

2.拓扑排序

​ 首先复习离散数学中的偏序集合与全序集合两个概念。

​ 若集合A中的二元关系R是自反的、非对称的和传递的,则R是A上的偏序关系。集合A与关系R一起称为一个偏序集合。

​ 若R是集合A上的一个偏序关系,如果对每个a、b∈A必有aRb或bRa ,则R是A上的全序关系。集合A与关系R一起称为一个全序集合。

​ 偏序关系经常出现在日常生活中。例如,若把A看成一项大的工程必须完成的一批活动,则aRb意味着活动a必须在活动b之前完成。比如,对于前面提到的计算机专业的学生必修的基础课与专业课,由于课程之间的先后依赖关系,某些课程必须在其它课程以前讲授,这里的aRb 就意味着课程a必须在课程b之前学完。

​ AOV网所代表的一项工程中活动的集合显然是一个偏序集合。为了保证该项工程得以顺利完成,必须保证AOV网中不出现回路。

​ 测试AOV网是否具有回路(即是否是一个有向无环图)的方法是在AOV网的偏序集合下构造一个线性序列,该线性序列具有以下性质:

​ ①在AOV网中,若顶点i 优先于顶点j ,则在线性序列中顶点i仍然优先于顶点j;

​ ②对于网中原来没有优先关系的顶点与顶点,在线性序列中也建立一个先后关系,或者顶点i优先于顶点j ,或者顶点j 优先于i。

​ 满足这样性质的线性序列称为拓扑有序序列。构造拓扑序列的过程称为拓扑排序。

​ 若某个AOV网中所有顶点都在它的拓扑序列中,则说明该AOV网不会存在回路,这时的拓扑序列集合是AOV网中所有活动的一个全序集合。

3.拓扑排序算法

​ 对AOV网进行拓扑排序的方法和步骤是:

​ ①从AOV网中选择一个没有前驱的顶点(即顶点的入度为0)并且输出它;

​ ②从网中删去该顶点,并且删去从该顶点发出的全部有向边;

​ ③重复前两步,直到剩余的网中不再存在没有前驱的顶点为止。

​ 这样操作的结果有两种:一是网中全部顶点都被输出,这说明网中不存在有向回路;一是网中顶点未全部输出,剩余的顶点均不前驱顶点,这说明网中存在有向回路。

下图给出在一个AOV网上实施上述步骤的例子。在这里插入图片描述
为了实现上述算法,对AOV网采用邻接表存储方式,并且邻接表中顶点结点中增加一个记录顶点入度的数据域,即顶点结构设为:在这里插入图片描述
其中,vertex、firstedge的含义如前所述;indegree为记录顶点入度的数据域。边结点的结构同8.2.2节所述。在这里插入图片描述在这里插入图片描述

顶点表结点结构的描述改为:

typedef struct vnode{ /顶点表结点/

​ int indegree /存放顶点入度/

​ VertexType vertex; /顶点域/

​ EdgeNode * firstedge; /边表头指针/

​ }VertexNode;

​ 算法中可设置一个堆栈,凡是网中入度为0 的顶点都将其入栈。为此,拓扑排序的算法步骤为:

​ ①将没有前驱的顶点(indegree域为0)压入栈;

​ ②从栈中退出栈顶元素输出,并把该顶点引出的所有有向边删去,即把它的各个邻接顶点的入度减1;

​ ③将新的入度为0的顶点再入堆栈;

​ ④重复②~④,直到栈为空为止。此时或者是已经输出全部顶点,或者剩下的顶点中没有入度为0的顶点。

​ 为了实现上述算法,对AOV网采用邻接表存储方式,并且邻接表中顶点结点中增加一个记录顶点入度的数据域,即顶点结构设为:在这里插入图片描述
其中,vertex、firstedge的含义如前所述;indegree为记录顶点入度的数据域。边结点的结构同8.2.2节所述。在这里插入图片描述在这里插入图片描述

顶点表结点结构的描述改为:

typedef struct vnode{ /顶点表结点/

​ int indegree /存放顶点入度/

​ VertexType vertex; /顶点域/

​ EdgeNode * firstedge; /边表头指针/

​ }VertexNode;

​ 算法中可设置一个堆栈,凡是网中入度为0 的顶点都将其入栈。为此,拓扑排序的算法步骤为:

​ ①将没有前驱的顶点(indegree域为0)压入栈;

​ ②从栈中退出栈顶元素输出,并把该顶点引出的所有有向边删去,即把它的各个邻接顶点的入度减1;

​ ③将新的入度为0的顶点再入堆栈;

​ ④重复②~④,直到栈为空为止。此时或者是已经输出全部顶点,或者剩下的顶点中没有入度为0的顶点。在这里插入图片描述
在这里插入图片描述

8.8.3 AOE图与关键路径

1.AOE网(Activity on edge network)

​ 若在带权的有向图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销,则此带权的有向图称为AOE网。

​ 如果用AOE网来表示一项工程,那么,仅仅考虑各个子工程之间的优先关系还不够,更多的是关心整个工程完成的最短时间是多少;哪些活动的延期将会影响整个工程的进度,而加速这些活动是否会提高整个工程的效率。因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。

​ AOE网具有以下两个性质:

​ ①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。

​ ②只有在进入一某顶点的各有向边所代表的活动都已经结束,该顶点所代表的事件才能发生。

​ 对于AOE网,可采用与AOV网一样的邻接表存储方式。其中,邻接表中边结点的域为该边的权值,即该有向边代表的活动所持续的时间。

​ 下图给出了一个具有15个活动、11个事件的假想工程的AOE网。v1,v2,… v11分别表示一个事件;<v1,v2>,<v1,v3>,…,<v10,v11>分别表示一个活动;用a1,a2, …,a15代表这些活动。其中,v1称为源点,是整个工程的开始点,其入度为0;v11为终点,是整个工程的结束点,其出度为0 。在这里插入图片描述

2.关键路径

​ 由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为源点到终点的最大路径长度(这里的路径长度是指该路径上的各个活动所需时间之和)。具有最大路径长度的路径称为关键路径。关键路径上的活动称为关键活动。

​ 关键路径长度是整个工程所需的最短工期。这就是说,要缩短整个工期,必须加快关键活动的进度。

​ 利用AOE网进行工程管理时要需解决的主要问题是:

​ ①计算完成整个工程的最短路径。

​ ②确定关键路径,以找出哪些活动是影响工程进度的关键。

3.关键路径的确定

​ 为了在AOE网中找出关键路径,需要定义几个参量,并且说明其计算方法。

​ ⑴事件的最早发生时间ve[k]

​ ve[k]是指从源点到顶点的最大路径长度代表的时间。这个时间决定了所有从顶点发出的有向边所代表的活动能够开工的最早时间。根据AOE网的性质,只有进入vk的所有活动<vj,vk>都结束时,vk代表的事件才能发生;而活动<vj, vk>的最早结束时间为ve[j]+dut(<vj, vk>)。所以计算vk发生的最早时间的方法如下:

​ ve[l]=0

​ ve[k]=Max{ve[j]+dut(< vj, vk>)} < vj, vk>∈p[k] (8-1)

其中,p[k]表示所有到达vk的有向边的集合;dut(<vj, vk>)为有向边< vj,vk>上的权值。

​ ⑵事件的最迟发生时间vl[k]

​ vl[k]是指在不推迟整个工期的前提下,事件vk允许的最晚发生时间。设有向边<vk,vj>代表从vk出发的活动,为了不拖延整个工期,vk发生的最迟时间必须保证不推迟从事件vk出发的所有活动<vk,vj>的终点vj的最迟时间vl[j]。vl[k] 的计算方法如下:

​ vl[n]=ve[n]

​ vl[k]=Min{vl[j]-dut(< vk- vj>)} < vk, vj>∈s[k] (8-2)

其中,s[k]为所有从vk发出的有向边的集合。

​ ⑶活动ai的最早开始时间e[i]

​ 若活动ai是由弧<vk,vj>表示,根据AOE网的性质,只有事件vk发生了,活动ai才能开始。也就是说,活动ai的最早开始时间应等于事件vk的最早发生时间。因此,有:

​ e[i]=ve[k] (8-3)在这里插入图片描述
⑷活动ai的最晚开始时间l[i]

​ 活动ai的最晚开始时间指,在不推迟整个工程完成日期的前提下, 必须开始的最晚时间。若由弧<vk,vj>表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后。因此,应该有:

​ l[i]=vl[j]-dut(<vk,vj>) (8-4)

​ 那些l[i]=e[i]的活动就是关键活动,而那些l[i]>e[i]的活动则不是关键活动,l[i]-e[i]的值为活动的时间余量。关键活动确定之后,关键活动所在的路径就是关键路径。

​ 下面以前图所示的AOE网为例,求出上述参量,来确定该网的关键活动和关键路径。

​ 首先,按照 (8-1)式求事件的最早发生时间ve[k]。

​ ve [1]=0

​ ve [2]=3

​ ve [3]=4

​ ve [4]=ve[2]+2=5

​ ve [5]=max{ve[2]+1,ve[3]+3}=7

​ ve [6]=ve[3]+5=9

​ ve [7]=max{ve[4]+6,ve[5]+8}=15

​ ve [8]=ve[5]+4=11

​ ve [9]=max{ve[8]+10,ve[6]+2}=21

​ ve [10]=max{ve[8]+4,ve[9]+1}=22

​ ve [11]=max{ve[7]+7,ve[10]+6}=28在这里插入图片描述
其次,按照 (8-2)式求事件的最迟发生时间vl[k]。

​ vl [11]= ve [11]=28

​ vl [10]= vl [11]-6=22

​ vl [9]= vl [10]-1=21

​ vl [8]=min{ vl [10]-4, vl [9]-10}=11

​ vl [7]= vl [11]-7=21

​ vl [6]= vl [9]-2=19

​ vl [5]=min{ vl [7]-8,vl [8]-4}=7

​ vl [4]= vl [7]-6=15

​ vl [3]=min{ vl [5]-3, vl [6]-5}=4

​ vl [2]=min{ vl [4]-2, vl [5]-1}=6

​ vl [1]=min{vl [2]-3, vl [3]-4}=0在这里插入图片描述
在这里插入图片描述
最后,比较e[i]和l[i]的值可判断出a2,a5,a9,a13,a14,a15是关键活动,关键路径如下图所示。在这里插入图片描述
由上述方法得到求关键路径的算法步骤为:

​ ⑴输入e条弧<j,k>,建立AOE-网的存储结构;

​ ⑵从源点v0出发,令ve[0]=0,按拓扑有序求其余各顶点的最早发生时间vei。如果得到的拓扑有序序列中顶点个数小于网中顶点数n,则说明网中存在环,不能求关键路径,算法终止;否则执行步骤⑶。

​ ⑶从汇点vn出发,令vl[n-1]=ve[n-1],按逆拓扑有序求其余各顶点的最迟发生时间vl[i] (n-2≥i≥2);

​ ⑷根据各顶点的ve和vl值,求每条弧s的最早开始时间e(s)和最迟开始时间1[s]。若某条弧满足条件e[s]=l[s],则为关键活动。

​ 由该步骤得到的算法参看算法8.20和8.21。在算法8.20中,Stack为栈的存储类型;引用的函数FindInDegree(G, indegree)用来求图G中各顶点的入度,并将所求的入度存放于一维数组indegree中。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值