数据结构-图
Posted lin546
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构-图相关的知识,希望对你有一定的参考价值。
一、图的基本概念
- 图、有向图、无向图就不解释了,注意有向边用<a,b>表示,无向边用(a,b)表示。
- 弧:在有向图中通常将边称为弧,含箭头的一端称为弧头,另一端称为弧尾,记作<vi,vj>。
- 顶点的度、入度和出度:在无向图中边记为(vi,vj),与顶点v相关的边的条数称为顶点v的度,在有向图中指向顶点v的边的条数称为v的入度,由顶点v发出的边的条数称为顶点v的出度。
- 有向完全图和无向完全图:若有向图中有n个顶点,则最多有n(n-1)条边(图中任意两个顶点都有两条边相连),将具有n(n-1)条边的有向图称为完全有向图。若无向图中有n个顶点,则最多有n(n-1)/2条边(任意两个顶点都有一条边),将具有n(n-1)/2条边的无向图称为无向完全图。
- 路径和路径长度:在一个图中,路径为相邻顶点序偶所构成的序列。路径长度是指路径上边的数目。
- 简单路径:序列中顶点不重复出现的路径称为简单路径。
- 回路:第一个顶点和最后一个顶点相同的路径。
- 简单回路:除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路。
- 连通、连通图和连通分量:在无向图中,如果从顶点vi到顶点vj有路径,则称vi和vj连通。如果图中任意两个顶点之间都连通,则称该图为连通图,否则将其中的极大连通子图称为连通分量。如下图a,b就是c的两个两个连通分量。
- 强连通图和强连通分量 在有向图中,若从vi到vj有路径,则称vi和vj是连通的,如果对于每一对顶点vi和vj,从vi到vj和从vj到vi都有路径,则称该图为强连通图,否则,将其中的极大强连通子图称为强连通分量。
- 权和网:图中每条边都可以附有一个对应的数,这种与边相关的数称为权,权可以表示从一个顶点到另一个顶点的距离或者花费的代价。边上带有权的图称为带权图,也称为网。
- 生成树:一个含 n 个顶点的连通图的生成树是该图中的一个极小连通子图,它包含图中 n 个顶点和足以构成一棵树的 n-1 条边。
- 生成森林:对于非连通图,对其每个连通分量可以构造一棵生成树,合成起来就是一个生成森林。
二、图的存储结构
2.1 邻接矩阵
?图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息。设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
看一个实例,下图左就是一个无向图。
从上面可以看出,无向图的边数组是一个对称矩阵。
而有向图讲究入度和出度,下面是一个有向图样例。
若图G是网图(带权图),有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
这里的wij表示(vi,vj)上的权值。和无权图不同的是若无边存在,则无权图的0改成正无穷。
?那么邻接矩阵是如何实现图的创建的呢?代码如下。
typedef char VertexType; //顶点类型
typedef int EdgeType; //边权值类型
#define MAXVEX 100
#define INF 65535 //用65535来代表无穷大
typedef struct
{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作边
int numVertexes, numEdges; //图中当前的顶点数和边数
}Graph;
void CreateGraph(Graph *G)
{
int i,j,k,w;
printf("输入顶点数和边数:
");
scanf("%d%d",&G->numVertexes,&G->numEdges);
getchar();
printf("输入%d个顶点符号:
",G->numVertexes);
for(i=0;i<G->numVertexes;i++)
scanf("%c",&G->vexs[i]);
getchar();
for(i=0;i<G->numVertexes;i++)
for(j=0;j<G->numVertexes;j++)
G->arc[i][j]=INF; //初始化邻接矩阵
for(k=0;k<2*G->numEdges;k++)//循环次数:无向图G->numEdges次,有向图G->numEdges*2次
{
printf("输入边(vi,vj)上的下标i,j和权w:");//如果是有向图,就按照方向输入下标
scanf("%d%d%d",&i,&j,&w);
G->arc[i][j]=w;
//G->arc[j][i]=G->arc[i][j];//有向图去掉这句
}
}
特点:
无向图的邻接矩阵对称,可压缩存储;有n个顶点的无向图需存储空间为n(n+1)/2。
有向图邻接矩阵不一定对称;有n个顶点的有向图需存储空间为n2。
无向图中顶点Vi的度TD(Vi)是邻接矩阵A中第i行元素之和。
有向图中,
顶点Vi的出度是A中第i行元素之和。顶点Vi的入度是A中第i列元素之和。
邻接矩阵的优缺点
优点:容易判定顶点间有无边(弧)和计算顶点的度(出度、入度)。
缺点:边数较少时,空间浪费较大。
2.2 邻接表
引入原因:邻接矩阵在稀疏图时空间浪费较大。因此,找到一种数组与链表相结合的存储方法称为邻接表。
2.2.1、实现
为图中每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点Vi的边(有向图中指以Vi为尾的弧)。例如,下图就是一个无向图的邻接表的结构。?
若是有向图邻接表结构是类似的,但要注意的是有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样就很容易的到每个顶点的出度。但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点vi都建立一个链接为vi为弧头的表。
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。如下图所示。?
对于邻接表结构,图的建立代码如下。(无向图)
#define MAXVEX 1000 //最大顶点数
typedef char VertexType; //顶点类型
typedef int EdgeType; //边上权值类型
typedef struct EdgeNode //边表结点
{
int adjvex; //邻接点域,存储该顶点对应的下标
EdgeType weigth; //用于存储权值,对于非网图可以不需要
struct EdgeNode *next; //链域,指向下一个邻接点
}EdgeNode;
typedef struct VertexNode //顶点表结构
{
VertexType data; //顶点域,存储顶点信息
EdgeNode *firstedge; //边表头指针
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes, numEdges; //图中当前顶点数和边数
}GraphList;
//建立图的邻接表结构
void CreateGraph(GraphList *g)
{
int i, j, k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d%d", &g->numVertexes, &g->numEdges);
getchar();
for(i = 0; i <g->numVertexes; i++)
{
printf("请一次一个输入顶点%d:
", i);
scanf("%c",&g->adjList[i].data); //输入顶点信息
getchar();
g->adjList[i].firstedge = NULL; //将边表置为空表
}
g->adjList[i].firstedge = NULL;
//建立边表
for(k = 0; k < g->numEdges; k++)//关于邻接表的循环次数无向图与与有向图都是g->numEdges次
{
printf("输入无向图边(vi,vj)上的顶点序号和权值:
");
int w;
scanf("%d%d%d",&i,&j,&w);
e =new EdgeNode;
e->adjvex = j; //邻接序号为j
e->weigth = w; //边<vi,vj>的权值
e->next = g->adjList[i].firstedge;//将e指针指向当前顶点指向的结构
g->adjList[i].firstedge = e;//将当前顶点的指针指向e
e = new EdgeNode;
e->adjvex =i;
e->weigth = w; //边<vj,vi>的权值
e->next = g->adjList[j].firstedge;
g->adjList[j].firstedge = e;
}
}
2.3 十字链表
对于有向图来说邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。有没有可能把邻接表与逆邻接表结合起来呢?这就是是十字链表。
2.4 邻接多重表
2.4.1、引入原因
无向图的邻接表中每一条边有两个结点,给对图的边进行访问的操作带来不便。有些时候需要同时找到表示同一条边的两个结点(如删除一条边)。
2.4.2、实现方式
2.5 边集数组
变集数组是由两个一维数组构成。一个存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成,如下图。显然边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
三、图的遍历
3.1 图的深度优先遍历
3.1.1、主要思想
首先访问出发点v,并将其标记为已访问过;然后选取与v邻接的未被访问的任意一个顶点w,并访问它;再选取与w邻接的未被访问的任一顶点并访问,以此重复进行。当一个顶点所有的邻接顶点都被访问过时,则以此退回到最近被访问过的顶点,若该顶点还有其他邻接顶点未被访问,则从这些未被访问的顶点中取一个并重复上述访问过程,直至图中所有顶点都被访问过为止。
3.1.2、算法实现
显然深度优先遍历连通图是一个递归的过程。为了在遍历过程中便于区分顶点是否已经被访问,需设访问标志数组visited[n],其初始值为false。
采用邻接矩阵表示图的深度优先遍历
/*
从第v个顶点出发深度优先搜索遍历图
*/
void DFS_AM(AMGraph G,int v){
cout<<v;
visited[v] = true;
for (int w = 0; w < G.vexnum; w++)
{
//如果w是v的邻接点,且w未被访问,则递归调用DFS_AM
if ((G.arcs[v][w]!=0)&&(!visited[w]))
{
DFS_AM(G,w);
}
}
}
采用邻接表表示图的深度优先搜索遍历
void DFS_AL(ALGraph G,int v){
//图G为邻接表类型,从第v个顶点出发深度优先搜索遍历图G
cout<<v;
visited[v]=true;
while(p!=NULL){
w = p->adjvex; //表示w是v的邻接点
if(!visited[w]){//如果w未被访问,则递归调用DFS_AL
DFS_AL(G,w);
}
p = p->nextarc;
}
}
深度优先搜索遍历非连通图
若是非连通图,上述遍历过程执行之后,图中一定还有顶点未被访问,需要从图中另选一个未被访问的顶点作为起始点,重复上述深度优先搜索过程,直到图中所有顶点均被访问过为止。
void DFSTraverse(Graph G){
//对非连通图G做深度优先遍历
for(v=0;v<G.vexnum;v++){
visited[v] = false; //访问标志数组初始化
}
for(v =0;v<G.vexnum;v++){ //循环调用算法DFS(可以是DFS_AM或DFS_AL)
if(!visited[v]){
DFS(G,v); //对尚未访问的顶点调用DFS
}
}
}
3.2 图的广度优先遍历
3.2.1、主要思想??
首先以一个未被访问过的顶点作为起始顶点,访问其所有相邻的顶点,然后对每个相邻的顶点,再访问它们相邻的未被访问过的顶点,直到所有的顶点都被访问过,遍历结束。
广度搜索遍历图的时候,需要用到一个队列(二叉树的层次遍历也要用到队列),算法执行过程可简单概括如下:
1)任取图中一个顶点访问,入队,并将这个顶点标记为已访问。
2)当队列不空时循环执行:出队,依次检查出队顶点的所有邻接顶点,访问没有被访问过的邻接顶点将其入队。
3)当队列为空时跳出循环,广度优先搜索即完成。
3.2.2、算法实现
算法步骤:
- 从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队。
- 只要队列不空,则重复下列操作:
- 队头顶点u出队;
- 依次检查u的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true;
void BFS(Graph G,int v){
cout<<v;
visited[v] = true;
InitQueue(Q); //辅助队列Q初始化,置空
EnQueue(Q,v);
while (!QueueEmpty(Q)) //队列非空
{
DeQueue(Q,u); //队头元素出列并置为u
for (w = FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w)){
//依次检查u的所有邻接点w,FirstAdVex表示u的第一个结点,
//NextAdjVex表示u相对于w的下一个邻接点,w>=0表示存在邻接点
if (!visited[w]) //w为尚未访问的邻接顶点
{
cout<<w;
visited[w]=true;
EnQueue(Q,w);
}
}
}
}
四、图的应用
4.1、最小生成树
4.1.1、普里姆(prim)算法
从图中任意取出一个顶点,把它当成一棵树,然后从与这棵树相接的边中选取一条最短(权值最小)的边,并将这条边及其所连接的顶点也并入这棵树中,此时得到了一棵有两个顶点的树。然后从与这棵树相接的边中选取一条最短的边,并将这条边及其所连顶点并入当前树中,得到一棵有3个顶点的树。以此类推,直到图中所有顶点都被并入树中为止,此时得到的生成树就是最小生成树。
4.1.2、克鲁斯卡尔(Kruskal)算法
普里姆(prim)算法是以某顶点为起点,逐步找各个顶点上最小权值的边来构建最小生成树的。我们也可以直接以边为目标来构建,因为权值是在边上,直接去找最小权值的边来构建生成树,只不过构建时要考虑是否会形成环路而已。
思想:首先按照边的权值进行从小到大排序,每次从剩余的边中选择权值较小且边的两个顶点不在同一个集合内的边(就是不会产生回路的边),加入到生成树中,直到加入了n-1条边为止:。
比如下面这个图:
构建过程为:
最小生成树算法的分析
普里姆算法和图的边数无关,适合边稠密的情况。
克鲁斯卡尔算法适合边稀疏的情况。
4.2、最短路径
4.2.1、每一对顶点之间的最短路径-弗洛伊算法Floyd
下面为《啊哈算法》中内容。
暑假,小哼准备去一些城市旅游。小哼希望在出发之前知道任意两个城市之前的最短路程。
???????上图中有4个城市8条公路,公路上的数字表示这条公路的长短。请注意这些公路是单向的。我们现在需要求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。
我们使用邻接矩阵的来存储路径信息:
现在回到问题:如何求任意两点之间最短路径呢?通过之前的学习我们知道通过深度或广度优先搜索可以求出两点之间的最短路径。可是还有没有别的方法呢?
????? 根据我们以往的经验,如果要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。那么这个中转的顶点k是1~n中的哪个点呢?甚至有时候不只通过一个点,而是经过两个点或者更多点中转会更短,即a->k1->k2->b或者a->k1->k2…->k->i…->b。好,下面我们将这个问题一般化。
?当任意两点之间不允许经过第三个点时,这些城市之间最短路程就是初始路程,如下。
???如现在只允许经过1号顶点,求任意两点之间的最短路程,应该如何求呢?只需判断e[i][1]+e[1][j]是否比e[i][j]要小即可。
在只允许经过1号顶点的情况下,任意两点之间的最短路程更新为:
通过上图我们发现:在只通过1号顶点中转的情况下,3号顶点到2号顶点(e[3][2])、4号顶点到2号顶点(e[4][2])以及4号顶点到3号顶点(e[4][3])的路程都变短了。
????接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短路程。如何做呢?我们需要在只允许经过1号顶点时任意两点的最短路程的结果下,再判断如果经过2号顶点是否可以使得i号顶点到j号顶点之间的路程变得更短。即判断e[i][2]+e[2][j]是否比e[i][j]要小,代码实现为如下。
//经过1号顶点
for(i=1;i<=n;i++)
????for(j=1;j<=n;j++)
????????if?(e[i][j] > e[i][1]+e[1][j])??e[i][j]=e[i][1]+e[1][j];
//经过2号顶点
for(i=1;i<=n;i++)
????for(j=1;j<=n;j++)
????????if?(e[i][j] > e[i][2]+e[2][j])??e[i][j]=e[i][2]+e[2][j];
在只允许经过1和2号顶点的情况下,任意两点之间的最短路程更新为:
??通过上图得知,在相比只允许通过1号顶点进行中转的情况下,这里允许通过1和2号顶点进行中转,使得e[1][3]和e[4][3]的路程变得更短了。
??同理,继续在只允许经过1、2和3号顶点进行中转的情况下,求任意两点之间的最短路程。任意两点之间的最短路程更新为:
???最后允许通过所有顶点作为中转,任意两点之间最终的最短路程为:
???整个算法过程虽然说起来很麻烦,但是代码实现却非常简单,核心代码只有五行:
for(k=1;k<=n;k++)
????for(i=1;i<=n;i++)
????????for(j=1;j<=n;j++)
????????????if(e[i][j]>e[i][k]+e[k][j])
?????????????????e[i][j]=e[i][k]+e[k][j];
? ?这段代码的基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。其实这是一种“动态规划”的思想。
??通过这种方法我们可以求出任意两个点之间最短路径。它的时间复杂度是O(N3)。令人很震撼的是它竟然只有五行代码,实现起来非常容易。正是因为它实现起来非常容易,如果时间复杂度要求不高,使用Floyd-Warshall来求指定两点之间的最短路或者指定一个点到其余各个顶点的最短路径也是可行的。当然也有更快的算法,Dijkstra算法。 ??????
?另外需要注意的是:Floyd-Warshall算法不能解决带有“负权回路”(或者叫“负权环”)的图,因为带有“负权回路”的图没有最短路。例如下面这个图就不存在1号顶点到3号顶点的最短路径。因为1->2->3->1->2->3->…->1->2->3这样路径中,每绕一次1->-2>3这样的环,最短路就会减少1,永远找不到最短路。其实如果一个图中带有“负权回路”那么这个图则没有最短路。
4.2.2、从某个原点到其余各顶点的最短路径-迪杰斯特拉Djakarta
上文我们介绍了神奇的只有五行的Floyd最短路算法,它可以方便的求得任意两点的最短路径,这称为“多源最短路”。本周来来介绍指定一个点(源点)到其余各个顶点的最短路径,也叫做“单源最短路径”。例如求下图中的1号顶点到2、3、4、5、6号顶点的最短路径。
? ? ? ?与Floyd-Warshall算法一样这里仍然使用二维数组e来存储顶点之间边的关系,初始值如下。
我们还需要用一个一维数组dis来存储1号顶点到其余各个顶点的初始路程,如下。
?我们将此时dis数组中的值称为最短路的“估计值”。
? ? ? ?既然是求1号顶点到其余各个顶点的最短路程,那就先找一个离1号顶点最近的顶点。通过数组dis可知当前离1号顶点最近是2号顶点。当选择了2号顶点后,dis[2]的值就已经从“估计值”变为了“确定值”,即1号顶点到2号顶点的最短路程就是当前dis[2]值。为什么呢?你想啊,目前离1号顶点最近的是2号顶点,并且这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得1号顶点到2号顶点的路程进一步缩短了。
? ? 既然选了2号顶点,接下来再来看2号顶点有哪些出边呢。有2->3和2->4这两条边。先讨论通过2->3这条边能否让1号顶点到3号顶点的路程变短。也就是说现在来比较dis[3]和dis[2]+e[2][3]的大小。其中dis[3]表示1号顶点到3号顶点的路程。dis[2]+e[2][3]中 dis[2]表示1号顶点到2号顶点的路程,e[2][3]表示2->3这条边。所以dis[2]+e[2][3]就表示从1号顶点先到2号顶点,再通过2->3这条边,到达3号顶点的路程。
? ? ? ?我们发现dis[3]=12,dis[2]+e[2][3]=1+9=10,dis[3]>dis[2]+e[2][3],因此dis[3]要更新为10。这个过程有个专业术语叫做“松弛”。即1号顶点到3号顶点的路程即dis[3],通过2->3这条边松弛成功。这便是Dijkstra算法的主要思想:通过“边”来松弛1号顶点到其余各个顶点的路程。
????? 同理通过2->4(e[2][4]),可以将dis[4]的值从∞松弛为4。
? ? ? 刚才我们对2号顶点所有的出边进行了松弛。松弛完毕之后dis数组为:
? ? ? ?接下来,继续在剩下的3、4、5和6号顶点中,选出离1号顶点最近的顶点。通过上面更新过dis数组,当前离1号顶点最近是4号顶点。此时,dis[4]的值已经从“估计值”变为了“确定值”。下面继续对4号顶点的所有出边(4->3,4->5和4->6)用刚才的方法进行松弛。松弛完毕之后dis数组为:
? ? ? ?继续在剩下的3、5和6号顶点中,选出离1号顶点最近的顶点,这次选择3号顶点。此时,dis[3]的值已经从“估计值”变为了“确定值”。对3号顶点的所有出边(3->5)进行松弛。松弛完毕之后dis数组为:
? ? ? ?继续在剩下的5和6号顶点中,选出离1号顶点最近的顶点,这次选择5号顶点。此时,dis[5]的值已经从“估计值”变为了“确定值”。对5号顶点的所有出边(5->4)进行松弛。松弛完毕之后dis数组为:
? ? ? ?最后对6号顶点所有点出边进行松弛。因为这个例子中6号顶点没有出边,因此不用处理。到此,dis数组中所有的值都已经从“估计值”变为了“确定值”。
? ? ? ?最终dis数组如下,这便是1号顶点到其余各个顶点的最短路径。
? ? ? ?OK,现在来总结一下刚才的算法。算法的基本思想是:每次找到离源点(上面例子的源点就是1号顶点)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。基本步骤如下:
将所有的顶点分为两部分:已知最短路程的顶点集合P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。我们这里用一个book[i]数组来记录哪些点在集合P中。例如对于某个顶点i,如果book[i]为1则表示这个顶点在集合P中,如果book[i]为0则表示这个顶点在集合Q中。
设置源点s到自己的最短路径为0即dis=0。若存在源点有能直接到达的顶点i,则把dis[i]设为e[s][i]。同时把所有其它(源点不能直接到达的)顶点的最短路径为设为∞。
在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以点u为起点的边,对每一条边进行松弛操作。例如存在一条从u到v的边,那么可以通过将边u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u]+e[u][v]。如果这个值比目前已知的dis[v的值要小,我们可以用新值来替代当前dis[v]中的值。
重复第3步,如果集合Q为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。
? ?完整的Dijkstra[算法]代码如下:
#include <stdio.h>
int main()
{
int e[10][10],dis[10],book[10],i,j,n,m,t1,t2,t3,u,v,min;
int inf=99999999; //用inf(infinity的缩写)存储一个我们认为的正无穷值
//读入n和m,n表示顶点个数,m表示边的条数
scanf("%d %d",&n,&m);
//初始化
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if(i==j) e[i][j]=0;
else e[i][j]=inf;
//读入边
for(i=1;i<=m;i++)
{
scanf("%d %d %d",&t1,&t2,&t3);
e[t1][t2]=t3;
}
//初始化dis数组,这里是1号顶点到其余各个顶点的初始路程
for(i=1;i<=n;i++)
dis[i]=e[1][i];
//book数组初始化
for(i=1;i<=n;i++)
book[i]=0;
book[1]=1;
//Dijkstra算法核心语句
for(i=1;i<=n-1;i++)
{
//找到离1号顶点最近的顶点
min=inf;
for(j=1;j<=n;j++)
{
if(book[j]==0 && dis[j]<min)
{
min=dis[j];
u=j;
}
}
book[u]=1;
for(v=1;v<=n;v++)
{
if(e[u][v]<inf)
{
if(dis[v]>dis[u]+e[u][v])
dis[v]=dis[u]+e[u][v];
}
}
}
//输出最终的结果
for(i=1;i<=n;i++)
printf("%d ",dis[i]);
return 0;
}
可以输入以下数据进行验证。第一行两个整数n ?m。n表示顶点个数(顶点编号为1~n),m表示边的条数。接下来m行表示,每行有3个数x y z。表示顶点x到顶点y边的权值为z。
6 9
1 2 1
1 3 12
2 3 9
2 4 3
3 5 5
4 3 4
4 5 13
4 6 15
5 6 4
运行结果是
0 1 8 4 13 17
通过上面的代码我们可以看出,这个算法的时间复杂度是O(N*2*N)即O(N2)。其中每次找到离1号顶点最近的顶点的时间复杂度是O(N),这里我们可以用“堆”(以后再说)来优化,使得这一部分的时间复杂度降低到O(logN)。另外对于边数M少于N2的稀疏图来说(我们把M远小于N2的图称为稀疏图,而M相对较大的图称为稠密图),我们可以用邻接表(这是个神马东西?不要着急,下周再仔细讲解)来代替邻接矩阵,使得整个时间复杂度优化到O(MlogN)。请注意!在最坏的情况下M就是N2,这样的话MlogN要比N2还要大。但是大多数情况下并不会有那么多边,因此MlogN要比N2小很多。
3、拓扑排序
3.1 AOV网
在一个表示没有回路的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的图,我们成为AOV网。
3.2、拓扑序列
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2,...,vn满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj之前。则我们称这样的顶点序列为一个拓扑序列。
在一个有向图中找到一个拓扑序列的过程如下:
① 从有向图中选择一个没有前驱(入度为0)的顶点输出。
② 删除①中的顶点,并且删除从该顶点发出的全部边。
③ 重复上述两步,直到剩余的网中不存在没有前驱的顶点为止
??? 所谓拓扑排序,其实就是对一个有向图构造拓扑排序的过程。构造时会有两个结果,如果此网的全部顶点都被输出,则说明它是不存在环的AOV网;如果输出顶点少了,哪怕是少了一个,也说明这个网存在环,不是AOV网。
3.3 拓扑排序算法
由于拓扑排序的过程中,需要删除顶点,显然用邻接表会更加方便。因此我们需要为AOV网建立一个邻接表。考虑到算法过程中始终要查找入度为0的顶点,我们在原来顶点表结点结构中增加一个入度域in,结构如下,其中in就是入度的数字。
对于下面第一幅图AOV网,我们可以得到第二幅图的邻接表数据结构
在拓扑排序算法中,涉及的结构代码如下:
在算法中,我们还需要一个栈来存储处理过程中入度为0的顶点。下面是核心代码:
1、程序开始运行,第3~7行都是变量的定义,其中stack是一个栈,用来存储整型的数字。
2、第8~11行,作了一个循环判断,把入度为0的顶点都入栈,如下:
3、第12~23行,while循环,当栈中有数据元素时,始终循环。
4、第14~16行,v3出栈得到gettop=3。并打印此顶点,然后count加1.
5、第17~22行,循环其实是对v3顶点对应的弧链表进行遍历,找到v3连接的两个顶点v2和v13,并将它们的入度减少一位,此时v2和v3的in值都为1(如果为0就将其入栈),它的目的是为了将v3顶点上的弧删除。
6、再次循环,第12~23行。此时处理的是顶点v1,经过出栈、打印、count=2后,我们对v1到v2、v4、v8的弧进行了遍历。并同样减少了它们的入度数,此时v2入度为0,于是由第20~21克制,v2入栈。
7、接下来就是同样的处理方式了。下面展示了v2 v6 v0 v4 v5 v8的打印删除过程,后面还剩几个顶点都类似,就不图示了。
8、最终打印结果为3->1->2->6->0->4->5->8->7->12->9->10->13->11。当然这结果并不是唯一的拓扑排序方案。
3.4 逆拓扑排序
若AOV网中考察各顶点的出度并以下列顺序进行排序,则将这种排序称为逆拓扑排序,输出的结果称为逆拓扑排序有序序列。
- 在网中选择一个没有后继的顶点(出度为0)输出。
- 在网中删除该顶点,并删除多有到达该顶点的边。
- 重复上述两步,知道AOV网中已无出度为0的顶点为止。
4、关键路径
AOE网(Activity On Edge),即以边表示活动的网。AOE网是一个带权的有向无环网,其中,顶点表示事件,弧表示活动,权表示活动持续的事件。通常AOE网用来估算工程完成的时间。
4.1、关键路径的几个术语
- 路径长度:路径上各活动持续时间之和
- 关键路径:路径长度最长的路径
- 关键活动:关键路径上的活动,即这 些活动的时间延迟或提前影响整个工期的延迟或提前。
那么现在的问题就是如何找到关键路径。我们直接给出结论:
我们只需要找到所有活动的最早开始时间和最晚开始时间,并且比较它们,如果相等就意味着此活动是关键活动,活动间的路径为关键路径。如果不等,则就不是。
为此我们定义如下几个参数。
1 关键路径算法
我们将AOE网转化为邻接表结构,与拓扑排序是邻接表不同的是,这里弧链增加了weight域,用来存储弧的权值。
计算事件的最早发生时间etv,即顶点vk的最早发生时间的过程如下:
①进行拓扑排序,得到各顶点的拓扑排序序列。
②按照上述拓扑排序序列的顺序,依次求出各顶点所代表事件的最早发生时间。公式是:
其中P[k]表示所有到达顶点vk的弧的集合。比如下图的P[3]就是<v1,v3>和<v2,v3>两条弧。len<v1,v2>是弧<vi,vj>上的权值。
计算活动的最晚开工时间lte,即弧ak的最晚发生时间,也就是不推迟工期的最晚开工时间的过程如下:
①进行逆拓扑排序,得到各顶点的拓扑排序序列。②按照上述拓扑排序序列的顺序,依次求出各顶点所代表事件的最迟发生时间。公式是:
其中S[k]表示所有从顶点Vk出发的弧的集合。比如下图的S[4]就是<v4,v6>和<v4,v7>两条弧,en<vk,vj>是弧<vk,vj>上的权值。
最后结果为:
所以最终就是判断ete与lte是否相等,相等意味着活动没有任何空闲,是关键活动,否则就不是。我们很容易得到关键活动是0,2,3,4,7,8,9。注意,关键路径不止一条。最终关键路径如图:
参考:《啊哈算法》《大话数据结构》《天勤考研》
以上是关于数据结构-图的主要内容,如果未能解决你的问题,请参考以下文章