(五)图 911

  1. 理解图的定义和基本术语、图的存储结构,主要指邻接矩阵和邻接表。
  2. 熟练掌握图的深度和广度优先搜索遍历、产生图的最小生成树的普利姆算法和克鲁斯卡尔算法。
  3. 熟练掌握最短路径的狄克斯特拉算法和佛洛伊德算法。
  4. 理解拓扑排序的概念及算法、关键路径的概念及算法。
  5. 理解最小生成树和最短路的生活应用。

1. 图的定义和基本术语、图的存储结构

定义

书P199
图是由两个集合构成,一个是非空但有限的顶点集合V,另一个是描述顶点之间关系——边的集合E。|V|表示顶点的数量,|E|表示边的数量。
至少要求有一个顶点,但边集可以为空。

基本术语

书P200
无向图:(w,v)
有向图:<w,v>
简单图:如果出现重边或自回路边,就叫非简单图。
邻接点:(w,v)叫做w和v互为邻接点;<w,v>叫做起点v邻接到终点w,或叫做终点w邻接自起点v。
路径:一条路径的长度是这条路径所包含的边数。
简单路径:指除了路径的首位顶点外,其余顶点都是不同的。
回路:路径长度为1的回路叫做自回路。简单路径形成的回路叫做简单回路。
无环图:一个有向图中不存在回路,叫做无环图。
无向完全图:任意两顶点都有一条边相连接的无向图。在一个含有n个顶点的无向完全图中,共有n(n-1)/2条边。
有向完全图:任意两顶点之间都由方向互为相反的两条弧相连接。在一个含有n个顶点的有向完全图中,共有n(n-1)条弧。
顶点的度:依附于该顶点的边数。入度指以该顶点为终点的弧的数目,出度指以该顶点为起点的弧的数目。度=入度+出度
稠密图:图的边数接近完全图的边数。
稀疏图:边数很少的图。
权:边可以附带一个数值信息,称为权或代价。边上带权的图称为网图。
子图:对于图G=(V,E)和G’(V‘,E’),若满足V‘是V的子集,并且E’是E的子集,则称图G‘是G的一个子图。
连通图:无向图中,图中任意两顶点都是连通的。无向图的极大连通子图称为连通分量。连通的无向图只有一个连通分量(就是本图),不连通的无向图有多个连通分量。
强连通图:有向图中,任意两个顶点都存在来回双向路径。有向图的极大强连通子图称为强联通分量。不是强连通的有向图有多个强连通分量。
生成树:连通图G的生成树,是G的包含其全部n个顶点的一个极小连通子图。必定包含且仅包含n-1条边。生成树可能不唯一。
生成森林:非连通图中,每个连通分量都是一个极小连通子图,一棵生成树可以对应一个连通分量。对无向图而言,生成森林中树的数量就等于它的连通分量数。

存储结构

邻接矩阵

书P206
对于没有权值的图,能到达的顶点是1,不能到达的顶点和它本身是0。
对于有权值的图,能到达的顶点是权值,不能到达的顶点和它本身是∞或负数。
无向图的邻接矩阵一定是一个对称矩阵,所需存储元素的个数是|V|×(|V|-1)/2。
花费(|V|2)的存储空间,对稠密图而言高效,对稀疏图而言浪费空间。

邻接表

书P210
顶点表:由顶点数据域和指向第一条邻接边的指针域构成。
边表:由邻接点域和指向下一条邻接边的指针域构成。网图的边表需要再增设一个存储权值的域。
若无向图有|V|个顶点和|E|条边,邻接表需要|V|个头结点和2|E|个表结点。在边稀疏的情况下,邻接表比邻接矩阵节省存储空间。
邻接表容易找到任一顶点的第一个邻接点和下一个邻接点。但要判断任意两个顶点之间是否有边或弧相连,不及邻接矩阵方便。
建立邻接表或逆邻接表的时间复杂度为O(|V|+|E|),花费(|V|2)的存储空间。

2.图的遍历、最小生成树

深度优先搜索(DFS)

书P218
类似于树的先序遍历,一条路走到底。
用邻接表存储图,O(N+E)。
用邻接矩阵存储图,O(N2)。

/* Visited[]为全局变量,已经初始化为false */
void DFS( Graph G, Vertex V, void (*Visit)(Vertex) )
{   /* 从第V个顶点出发递归地深度优先遍历图G */
	Visit( V ); /* 访问第V个顶点 */
	Visited[V] = true;
	for ( V 的每个邻接点 W )
		if ( !Visited[W] )
			/* 对V的尚未访问的邻接顶点W递归调用DFS */
			DFS( G, W, Visit );
}

广度优先搜索(BFS)

书P220
类似于树的层次遍历,应用:验证六度空间理论问题。
用邻接表存储图,O(N+E)。
用邻接矩阵存储图,O(N2)。

/* IsEdge(Graph, V, W)检查<V, W>是否图Graph中的一条边,即W是否V的邻接点。  */
/* 此函数根据图的不同类型要做不同的实现,关键取决于对不存在的边的表示方法。*/
/* 例如对有权图, 如果不存在的边被初始化为INFINITY, 则函数实现如下:         */
bool IsEdge( MGraph Graph, Vertex V, Vertex W )
{
    return Graph->G[V][W]<INFINITY ? true : false;
}

/* Visited[]为全局变量,已经初始化为false */
void BFS ( MGraph Graph, Vertex S, void (*Visit)(Vertex) )
{   /* 以S为出发点对邻接矩阵存储的图Graph进行BFS搜索 */
	Queue Q; 	
	Vertex V, W;

	Q = CreateQueue( MaxSize ); /* 创建空队列, MaxSize为外部定义的常数 */
	/* 访问顶点S:此处可根据具体访问需要改写 */
	Visit( S );
	Visited[S] = true; /* 标记S已访问 */
    AddQ(Q, S); /* S入队列 */
    
	while ( !IsEmpty(Q) ) {
		V = DeleteQ(Q);  /* 弹出V */
		for( W=0; W<Graph->Nv; W++ ) /* 对图中的每个顶点W */
			/* 若W是V的邻接点并且未访问过 */
			if ( !Visited[W] && IsEdge(Graph, V, W) ) {
                /* 访问顶点W */
				Visit( W );
				Visited[W] = true; /* 标记W已访问 */
                AddQ(Q, W); /* W入队列 */
			}
	} /* while结束*/
}

普利姆算法(Prim)

书P225
在所有的已选择边里,找相邻的最短边。
用邻接矩阵存储,适用于稠密图,时间复杂度O(|V|2)。
过程图示

#define ERROR -1 /* 错误标记,表示生成树不存在 */

int Prim( MGraph Graph, LGraph MST )
{ /* 将最小生成树保存为邻接表存储的图MST,返回最小权重和 */
	WeightType dist[MaxVertexNum], TotalWeight;
    Vertex parent[MaxVertexNum], V, W;
    int VCount;
	Edge E;
	
    /* 初始化。默认初始点下标是0 */
   	for (V=0; V<Graph->Nv; V++) {
        /* 这里假设若V到W没有直接的边,则Graph->G[V][W]定义为INFINITY */
   	    dist[V] = Graph->G[0][V];
   	    parent[V] = 0; /* 暂且定义所有顶点的父结点都是初始点0 */ 
    }
    TotalWeight = 0; /* 初始化权重和     */
    VCount = 0;      /* 初始化收录的顶点数 */
    /* 创建包含所有顶点但没有边的图。注意用邻接表版本 */
    MST = CreateGraph(Graph->Nv);
    E = (Edge)malloc( sizeof(struct ENode) ); /* 建立空的边结点 */
   	    
    /* 将初始点0收录进MST */
	dist[0] = 0;
    VCount ++;
	parent[0] = -1; /* 当前树根是0 */

	while (1) {
		V = FindMinDist( Graph, dist );
		/* V = 未被收录顶点中dist最小者 */
		if ( V==ERROR ) /* 若这样的V不存在 */
			break;   /* 算法结束 */
			
		/* 将V及相应的边<parent[V], V>收录进MST */
		E->V1 = parent[V];
		E->V2 = V;
		E->Weight = dist[V];
		InsertEdge( MST, E );
		TotalWeight += dist[V];
		dist[V] = 0;
		VCount++;
		
		for( W=0; W<Graph->Nv; W++ ) /* 对图中的每个顶点W */
			if ( dist[W]!=0 && Graph->G[V][W]<INFINITY ) {
			/* 若W是V的邻接点并且未被收录 */
				if ( Graph->G[V][W] < dist[W] ) {
				/* 若收录V使得dist[W]变小 */
					dist[W] = Graph->G[V][W]; /* 更新dist[W] */
					parent[W] = V; /* 更新树 */
				}
			}
	} /* while结束*/
	if ( VCount < Graph->Nv ) /* MST中收的顶点不到|V|个 */
	   TotalWeight = ERROR;
	return TotalWeight;   /* 算法执行完毕,返回最小权重和或错误标记 */
}

克鲁斯卡尔算法(Kruskal)

书P232
找权值最小的边,不管是否相邻。
用邻接表存储,适用于稀疏图,时间复杂度O(|E|log|V|)。
过程图示

int Kruskal( LGraph Graph, LGraph MST )
{ /* 将最小生成树保存为邻接表存储的图MST,返回最小权重和 */
	MST = 包含所有顶点但没有边的图;
    while( MST中收集的边不到Graph->Nv-1条 && 原图的边集E非空 ) {
        从E中选择最小代价边(V, W); /* 引入最小堆完成 */
        从E中删除此边(V, W);
        if  ( (V, W)的选取不在MST中构成回路 ) /* 此判断由并查集的Find完成 */
	        将(V, W)加入MST ; /* 此步由并查集的Union完成 */
        else     
	        彻底丢弃(V, W);
    } /* 结束while */
    if( MST中收集的边不到Graph->Nv-1条 )
		return ERROR;
	else
		return 最小权重和;
}

3.最短路径

狄克斯特拉算法(Dijkstra)

书P236
单源最短路径:从固定源点出发,求其到所有其他顶点的最短路径。
按路径长度递增的次序产生最短路径。
时间复杂度O(|V|2),适用于稠密图。稀疏图可以改进为O(|E|log|V|)。
过程图示

Vertex FindMinDist( MGraph Graph, int dist[], int collected[] )
{ /* 返回未被收录顶点中dist最小者 */
	Vertex MinV, V;
	int MinDist = INFINITY;

	for (V=0; V<Graph->Nv; V++) {
		if ( collected[V]==false && dist[V]<MinDist) {
			/* 若V未被收录,且dist[V]更小 */
			MinDist = dist[V]; /* 更新最小距离 */
			MinV = V; /* 更新对应顶点 */
		}
	}
	if (MinDist < INFINITY) /* 若找到最小dist */
		return MinV; /* 返回对应的顶点下标 */
	else return ERROR;  /* 若这样的顶点不存在,返回错误标记 */
}

bool Dijkstra( MGraph Graph, int dist[], int path[], Vertex S )
{
	int collected[MaxVertexNum];
	Vertex V, W;

	/* 初始化:此处默认邻接矩阵中不存在的边用INFINITY表示 */
	for ( V=0; V<Graph->Nv; V++ ) {
        dist[V] = Graph->G[S][V];
        if ( dist[V]<INFINITY )
           path[V] = S;
        else
           path[V] = -1;
        collected[V] = false;
    }
	/* 先将起点收入集合 */
	dist[S] = 0;
	collected[S] = true;

	while (1) {
        /* V = 未被收录顶点中dist最小者 */
		V = FindMinDist( Graph, dist, collected );
		if ( V==ERROR ) /* 若这样的V不存在 */
			break;      /* 算法结束 */
		collected[V] = true;  /* 收录V */
		for( W=0; W<Graph->Nv; W++ ) /* 对图中的每个顶点W */
		    /* 若W是V的邻接点并且未被收录 */
			if ( collected[W]==false && Graph->G[V][W]<INFINITY ) {
				if ( Graph->G[V][W]<0 ) /* 若有负边 */
					return false; /* 不能正确解决,返回错误标记 */
				/* 若收录V使得dist[W]变小 */
				if ( dist[V]+Graph->G[V][W] < dist[W] ) {
					dist[W] = dist[V]+Graph->G[V][W]; /* 更新dist[W] */
					path[W] = V; /* 更新S到W的路径 */
				}
			}
	} /* while结束*/
	return true; /* 算法执行完毕,返回正确标记 */
}

佛洛伊德算法

书P241
多源最短路径:求任意两顶点间的最短路径。
用邻接矩阵表示,到顶点本身是0,到到达不了的顶点是∞。
O(|V|3)适用于稠密图,O(|V|3+|E|×|V|)适用于稀疏图。
过程图示

bool Floyd( MGraph Graph, WeightType D[][MaxVertexNum], Vertex path[][MaxVertexNum] )
{
	Vertex i, j, k;

	/* 初始化 */
	for ( i=0; i<Graph->Nv; i++ )
		for( j=0; j<Graph->Nv; j++ ) {
			D[i][j] = Graph->G[i][j];
			path[i][j] = -1;
		}

	for( k=0; k<Graph->Nv; k++ )
		for( i=0; i<Graph->Nv; i++ )
			for( j=0; j<Graph->Nv; j++ )
				if( D[i][k] + D[k][j] < D[i][j] ) {
					D[i][j] = D[i][k] + D[k][j];
					if ( i==j && D[i][j]<0 ) /* 若发现负值圈 */
						return false; /* 不能正确解决,返回错误标记 */
					path[i][j] = k;
				}
	return true; /* 算法执行完毕,返回正确标记 */
}

4.拓扑排序、关键路径

拓扑排序

书P244
拓扑排序:用有向无环图(DAG)中各顶点构成有序序列。
依次找到任意一个入度为0的顶点,输出并删除。
一个顶点数|V|>1的有向图,如果每个顶点的入度都大于0,必定存在回路。

用数组存储,时间复杂度O(|V|2)。

bool TopSort( Graph Graph, Vertex TopOrder[] )
{ /* 对Graph进行拓扑排序,  TopOrder[]顺序存储排序后的顶点下标 */

	遍历图,得到各顶点的入度Indegree[];

	for( cnt=0; cnt<Graph->Nv; cnt++ ) {
		V = 未输出的入度为0的顶点;
		if ( 这样的V不存在 ) {
           printf("图中有回路");
           break;
        }
		TopOrder[cnt] = V; /* 将V存为结果序列的下一个元素 */
		/* 将V及其出边从图中删除 */
		for ( V的每个邻接点W )
			Indegree[W]--;
	}
    if ( cnt != Graph->Nv )
        return false; /* 说明图中有回路, 返回不成功标志 */ 
    else
        return true;
}

用队列存储,时间复杂度O(|V|+|E|),此算法可以用来检测有向图是否DAG。

bool TopSort( LGraph Graph, Vertex TopOrder[] )
{ /* 对Graph进行拓扑排序,  TopOrder[]顺序存储排序后的顶点下标 */
    int Indegree[MaxVertexNum], cnt;
    Vertex V;
    PtrToAdjVNode W;
   	Queue Q = CreateQueue( Graph->Nv );
 
    /* 初始化Indegree[] */
    for (V=0; V<Graph->Nv; V++)
        Indegree[V] = 0;
        
    /* 遍历图,得到Indegree[] */
    for (V=0; V<Graph->Nv; V++)
        for (W=Graph->G[V].FirstEdge; W; W=W->Next)
            Indegree[W->AdjV]++; /* 对有向边<V, W->AdjV>累计终点的入度 */
            
    /* 将所有入度为0的顶点入列 */
    for (V=0; V<Graph->Nv; V++)
        if ( Indegree[V]==0 )
            AddQ(Q, V);
            
    /* 下面进入拓扑排序 */ 
    cnt = 0; 
	while( !IsEmpty(Q) ){
		V = DeleteQ(Q); /* 弹出一个入度为0的顶点 */
		TopOrder[cnt++] = V; /* 将之存为结果序列的下一个元素 */
		/* 对V的每个邻接点W->AdjV */
		for ( W=Graph->G[V].FirstEdge; W; W=W->Next )
			if ( --Indegree[W->AdjV] == 0 )/* 若删除V使得W->AdjV入度为0 */
				AddQ(Q, W->AdjV); /* 则该顶点入列 */ 
	} /* while结束*/
    
    if ( cnt != Graph->Nv )
        return false; /* 说明图中有回路, 返回不成功标志 */ 
    else
        return true;
} 

关键路径

书P249
AOV图:用顶点表示活动或任务的图。
AOE图:有向边表示任务或活动,边上的权表示该活动持续的时间。
关键活动:不允许有时间延误的活动。
关键路径:由关键活动构成的从头至尾的路径,即由绝对不允许延误的活动组成的路径。

5.生活应用

最小生成树:公路村村通
最短路径:汽车导航系统

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值