任务9城市间公路网建设方案的选择.docx
《任务9城市间公路网建设方案的选择.docx》由会员分享,可在线阅读,更多相关《任务9城市间公路网建设方案的选择.docx(30页珍藏版)》请在冰豆网上搜索。
任务9城市间公路网建设方案的选择
任务9城市间公路网建设方案的选择
教学目标
1、知识目标
1)了解图的有关概念;
2)掌握图的邻接矩阵和邻接表的存储表示方法;
3)掌握图的遍历,深度优先搜索遍历和广度优先搜索遍历的算法;
4)掌握最小生成树的求解过程和算法。
2、能力目标
1)具有恰当的选择图作为数据的逻辑结构的能力;
2)具有应用图解决实际问题的能力。
3、素质目标
养成善于思考解决实际问题的良好习惯。
一、任务描述
有6个城市(A、B、C、D、E、F)如图9.1所示,已知每对城市间公路的建造费用,要求建造一个连接6个城市的交通网,使得任意两个城市间都可以直接或间接互达,要求使总的费用最小。
问:
如何建造6个城市间的公路网?
3
图9.1
二、相关知识
(一)图的基本概念
1、图(Graph)是由非空的顶点集合和一个描述顶点之间关系――边(或者弧)的集合组成,其形式化定义为:
G=(V,E)
其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合,集合E中P(vi,vj)表示顶点vi和顶点vj之间有一条直接连线,即偶对(vi,vj)表示一条边。
图9.2中G1给出了一个图的示例,在该图中:
集合V={v1,v2,v3,v4,v5};
集合E={(v1,v2),(v1,v4),(v2,v3),(v3,v4),(v3,v5),(v2,v5)}。
V2
V2
V1
V1
V3
V4
V3
V4
V5
图9.2无向图G1图9.2有向图G2
2、无向图。
在一个图中,如果任意两个顶点构成的偶对(vi,vj)∈E是无序的,即顶点之间的连线是没有方向的,则称该图为无向图。
如图9.2所示图G1是一个无向图。
3、有向图。
在一个图中,如果任意两个顶点构成的偶对(vi,vj)∈E是有序的,即顶点之间的连线是有方向的,则称该图为有向图。
如图9.2所示图G2是一个有向图:
G2=(V2,E2)
V2={v1,v2,v3,v4}
E2={,,,}
4、顶点、边、弧、弧头、弧尾。
图中,数据元素vi称为顶点(vertex);P(vi,vj)表示在顶点vi和顶点vj之间有一条直接连线。
如果是在无向图中,则称这条连线为边;如果是在有向图中,一般称这条连线为弧。
边用顶点的无序偶对(vi,vj)来表示,称顶点vi和顶点vj互为邻接点,边(vi,vj)依附于顶点vi与顶点vj;弧用顶点的有序偶对来表示,有序偶对的第一个结点vi被称为始点(或弧尾),在图中就是不带箭头的一端;有序偶对的第二个结点vj被称为终点(或弧头),在图中就是带箭头的一端。
5、无向完全图。
在一个无向图中,如果任意两顶点都有一条直接边相连接,则称该图为无向完全图。
可以证明,在一个含有n个顶点的无向完全图中,有n(n-1)/2条边。
6、有向完全图。
在一个有向图中,如果任意两顶点之间都有方向互为相反的两条弧相连接,则称该图为有向完全图。
在一个含有n个顶点的有向完全图中,有n(n-1)条边。
7、稠密图、稀疏图。
若一个图接近完全图,称为稠密图;称边数很少的图为稀疏图。
8、顶点的度、入度、出度。
顶点的度(degree)是指依附于某顶点v的边数,通常记为TD(v)。
在有向图中,要区别顶点的入度与出度的概念。
顶点v的入度是指以顶点为终点的弧的数目。
记为ID(v);顶点v出度是指以顶点v为始点的弧的数目,记为OD(v)。
有TD(v)=ID(v)+OD(v)。
例如,在G1中有:
TD(v1)=2TD(v2)=3TD(v3)=3TD(v4)=2TD(v5)=2
在G2中有:
ID(v1)=1OD(v1)=2TD(v1)=3
ID(v2)=1OD(v2)=0TD(v2)=1
ID(v3)=1OD(v3)=1TD(v3)=2
ID(v4)=1OD(v4)=1TD(v4)=2
n
∑
i=1
可以证明,对于具有n个顶点、e条边的图,顶点vi的度TD(vi)与顶点的个数以及边的数目满足关系:
2e=(
9、边的权、网图。
与边有关的数据信息称为权(weight)。
在实际应用中,权值可以有某种含义。
比如,在一个反映城市交通线路的图中,边上的权值可以表示该条线路的长度或者等级;对于一个电子线路图,边上的权值可以表示两个端点之间的电阻、电流或电压值;对于反映工程进度的图而言,边上的权值可以表示从前一个工程到后一个工程所需要的时间等等。
边上带权的图称为网图或网络(network)。
如图9.3所示,就是一个无向网图。
如果边是有方向的带权图,则就是一个有向网图。
10、路径、路径长度。
顶点vp到顶点vq之间的路径(path)是指顶点序列vp,vi1,vi2,…,vim,vq.。
其中,(vp,vi1),(vi1,vi2),…,(vim,.vq)分别为图中的边。
路径上边的数目称为路径长度。
图9.2所示的无向图G1中,v1→v4→v3→v5与v1→v2→v5是从顶点v1到顶点v5的两条路径,路径长度分别为3和2。
11、回路、简单路径、简单回路。
称vi的路径为回路或者环(cycle)。
序列中顶点不重复出现的路径称为简单路径。
在图9.2G1中,前面提到的v1到v5的两条路径都为简单路径。
除第一个顶点与最后一个顶点之外,其他顶点不重复出现的回路称为简单回路,或者简单环。
如图9.2G2中的v1→v3→v4→v1。
12、子图。
对于图G=(V,E),G’=(V’,E’),若存在V’是V的子集,E’是E的子集,则称图G’是G的一个子图。
图9.4示出了G2和G1的两个子图G’和G’’。
8
5
V1
V2
V1
B
A
2
3
V3
7
V5
V4
V4
V3
D
C
(a)G’(b)G’’
图9.3一个无向网图示意图9.4图G2和G1的两个子图示意
C
B
A
V2
V1
B
A
C
D
D
F
E
V4
V3
F
E
(a)无向图G3(b)G3的两个连通分量
图9.5无向图及连通分量示意图9.6有向图G2的两个强连通分量示意
12、连通的、连通图、连通分量。
在无向图中,如果从一个顶点vi到另一个顶点vj(i≠j)有路径,则称顶点vi和vj是连通的。
如果图中任意两顶点都是连通的,则称该图是连通图。
无向图的极大连通子图称为连通分量。
图9.5(a)中有两个连通分量,如图9.5(b)所示。
13、强连通图、强连通分量。
对于有向图来说,若图中任意一对顶点vi和vj(i≠j)均有从一个顶点vi到另一个顶点vj有路径,也有从vj到vi的路径,则称该有向图是强连通图。
有向图的极大强连通子图称为强连通分量。
图9.2G2中有两个强连通分量,分别是{v1,v2,v3}和{v4},如图9.6所示。
14、生成树。
所谓连通图G的生成树,是G的包含其全部n个顶点的一个极小连通子图。
它必定包含且仅包含G的n-1条边。
图9.4(b)G”示出了图9.2中G1的一棵生成树。
在生成树中添加任意一条属于原图中的边必定会产生回路,因为新添加的边使其所依附的两个顶点之间有了第二条路径。
若生成树中减少任意一条边,则必然成为非连通的。
15、生成森林。
在非连通图中,由每个连通分量都可得到一个极小连通子图,即一棵生成树。
这些连通分量的生成树就组成了一个非连通图的生成森林。
(二)图的存储结构
图的存储表示方法很多,这里介绍两种最常用的方法。
至于具体选择哪一种方法,主要取决于具体的应用和要施加的操作。
为了适合用C语言描述,以下假定顶点序号从0开始,即n个顶点图G顶点集是V(G)={v0,v1,…,vn-1}。
1、图的邻接矩阵表示法
1)图的邻接矩阵
设G=(V,E)是具有n个顶点的图,则G的邻接矩阵是元素具有如下性质的n阶方阵:
2)图的邻接矩阵表示法:
①用邻接矩阵表示顶点间的相邻关系;
②用一个顺序表来存储顶点信息。
0101
1011
0100
1100
A=
图9.7一个无向图的邻接矩阵表示
3)网络的邻接矩阵
若G是网络,则邻接矩阵可定义为:
其中:
wij表示边上的权值;∞表示一个计算机允许的、大于所有边上权值的数。
0
96∞963∞
2
1
39∞45∞
4A=64∞∞7
5735∞∞8
3
8
4
∞∞78∞
图9.8一个网图的邻接矩阵表示
4)图的邻接矩阵存储结构的描述
#defineVertexNum20//最大顶点数
typedefcharVertexType;//顶点类型定义
typedefintEdgeType;//权值类型定义
typedefstruct{
VertexTypevexs[VertexNum];//顶点表
EdgeTypeedges[VertexNum][VertexNum];//邻接矩阵,可看作边表
intn,e;//图中当前的顶点数和边数
}MGraph;
5)建立无向网络的算法
/*建立无向网络的算法:
*/
voidCreateMGraph(MGraph*G)
{//建立无向网(图)的邻接矩阵表示
inti,j,k,w;
scanf("%d%d",&G->n,&G->e);//输入顶点数边数
getchar();
printf("读入顶点信息,建立顶点表:
");
for(i=0;in;i++)//读入顶点信息,建立顶点表
G->vexs[i]=getchar();
getchar();
for(i=0;in;i++)
for(j=0;jn;j++)
G->edges[i][j]=0;//邻接矩阵初始化
for(k=0;ke;k++){//读入e条边,建立邻接矩阵
scanf("%d%d%d",&i,&j,&w);//输入权w
G->edges[i][j]=w;
G->edges[j][i]=w;
}
}
2、图的邻接表表示法
1)图的邻接表
对于图G中的每个顶点vi,把所有邻接于vi的顶点vj链成一个带头结点的单链表,这个单链表就称为顶点vi的邻接表(AdjacencyList)。
2)邻接表的结点结构
(1)表结点结构:
邻接表中每个表结点均有两个域
①邻接点域adjvex,存放与vi相邻接的顶点vj的序号j;
②链域next,将邻接表的所有表结点链在一起。
Ø注意:
若要表示边上的信息(如权值),则在表结点中还应增加一个数据域。
(2)头结点结构:
顶点vi邻接表的头结点包含两个域
①顶点域vertex,存放顶点vi的信息;
②指针域firstedge,vi的邻接表的头指针。
3)无向图的邻接表
对于无向图,vi的邻接表中每个表结点都对应于与vi相关联的一条边。
因此,将邻接表的表头向量称为顶点表。
将无向图的邻接表称为边表。
序号vertexfirstedge
0
V0
13∧
V1
1
023∧
∧
1
1
2
V2
V3
3
01∧
图9.9图的邻接表表示
Ø注意:
n个顶点e条边的无向图的邻接表表示中有n个顶点表结点和2e个边表结点。
4)有向图的邻接表
对于有向图,vi的邻接表中每个表结点都对应于以vi为始点射出的一条边。
因此,将有向图的邻接表称为出边表。
Ø注意:
n个顶点e条边的有向图,它的邻接表表示中有n个顶点表结点和e个边表结点。
5)有向图的逆邻接表
在有向图中,为图中每个顶点vi建立一个入边表的方法称逆邻接表表示法。
入边表中的每个表结点均对应一条以vi为终点(即射入vi)的边。
0
0
V1
V1
21∧3∧
0
∧
1
V2
∧
1
V2
2
V3
∧
3
2
V3
3
V4
3
V4
0∧
0∧2∧
(a)邻接表(b)逆邻接表
图9.10图9.2中G2的邻接表和逆邻接表
6)图的邻接表存储结构的描述
图的邻接表存储结构的描述
typedefstructnode{//边表结点定义
intadjvex;//邻接点域
structnode*next;//链域
//若要表示边上的权,则应增加一个数据域
}EdgeNode;
typedefstructvnode{//顶点表结点定义
VertexTypevertex;//顶点域
EdgeNode*firstedge;//边表头指针
}VertexNode;
typedefVertexNodeAdjList[VertexNum];//AdjList是邻接表类型
typedefstruct{//邻接表定义
AdjListadjlist;//邻接表
intn,e;//图中当前顶点数和边数
}ALGraph;
7)建立无向图的邻接表算法
建立无向图的邻接表算法
voidCreateALGraph(ALGraph*G)
{//建立无向图的邻接表表示
inti,j,k;
EdgeNode*s;
scanf("%d%d",&G->n,&G->e);//读入顶点数和边数
getchar();
for(i=0;in;i++){//建立顶点表
G->adjlist[i].vertex=getchar();//读入顶点信息
G->adjlist[i].firstedge=NULL;//边表置为空表
}
for(k=0;ke;k++){//建立边表
scanf("%d%d",&i,&j);//读入边(vi,vj)顶点对序号
s=(EdgeNode*)malloc(sizeof(EdgeNode));//生成边表结点
s->adjvex=j;//邻接点序号为j
s->next=G->adjlist[i].firstedge;
G->adjlist[i].firstedge=s;//将新结点*s插入顶点vi的边表头部
s=(EdgeNode*)malloc(sizeof(EdgeNode));
s->adjvex=i;//邻接点序号为i
s->next=G->adjlist[j].firstedge;
G->adjlist[j].firstedge=s;//将新结点*s插入顶点vj的边表头部
}
}
(三)图的遍历
1、图的遍历:
从图的某个顶点出发,沿着某条搜索路径对图中每个顶点各做一次且仅做一次访问,这一过程称为图的遍历。
它是许多图的算法的基础。
深度优先遍历和广度优先遍历是最为重要的两种遍历图的方法。
它们对无向图和有向图均适用。
Ø注意:
①由于图中任一顶点都可能和其它顶点相邻接,所以在遍历图时,访问了某顶点之后,有可能顺着某条回路又回到了该顶点。
为了避免重复访问同一个顶点,必须记住每个已访问的顶点。
为此,可设一向量visited[0…n-1],该向量的每个元素的初值均为0,如果访问了顶点Vi,就将visited[i]置为1,这样便可通过visited[i]的值来标志顶点Vi是否被访问过。
②以下假定遍历过程中访问顶点的操作是简单地输出顶点。
2、图的深度优先遍历
假设给定图G的初态是所有顶点均未曾访问过。
在G中任选一顶点v为初始出发点(源点)。
①递归定义:
首先访问出发点v,并将其标记为已访问过;然后依次从v出发搜索v的每个邻接点w。
若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点(亦称为从源点可达的顶点)均已被访问为止。
若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。
Ø说明:
图的深度优先遍历类似于树的前序遍历。
采用的搜索方法的特点是尽可能先对纵深方向进行搜索。
这种搜索方法称为深度优先搜索(Depth-FirstSearch)。
相应地,用此方法遍历图就很自然地称之为图的深度优先遍历。
②深度优先搜索的过程
设x是当前被访问顶点,在对x做过访问标记后,选择一条从x出发的未检测过的边(x,y)。
若发现顶点y已访问过,则重新选择另一条从x出发的未检测过的边,否则沿边(x,y)到达未曾访问过的y,对y访问并将其标记为已访问过;然后从y开始搜索,直到搜索完从y出发的所有路径,即访问完所有从y出发可达的顶点之后,才回溯到顶点x,并且再选择一条从x出发的未检测过的边。
上述过程直至从x出发的所有边都已检测过为止。
此时,若x不是源点,则回溯到在x之前被访问过的顶点;否则图中所有和源点有路径相通的顶点(即从源点可达的所有顶点)都已被访问过,若图G是连通图,则遍历过程结束,否则继续选择一个尚未被访问的顶点作为新源点,进行新的搜索过程。
③深度优先遍历的递归算法(DFS算法)
邻接矩阵表示的深度优先遍历算法
intvisited[VertexNum]={0};//定义标志向量
voidDFSM(MGraph*G,inti)
{//以vi为出发点进行搜索,设邻接矩阵是0,l矩阵
intj;
printf("%4c",G->vexs[i]);//访问顶点vi
visited[i]=1;
for(j=0;jn;j++)//依次搜索vi的邻接点
if((G->edges[i][j]==1)&&(!
visited[j]))
DFSM(G,j);//(vi,vj)∈E,且vj未访问过
}
voidDFSTraverse(MGraph*G)
{//深度优先遍历以邻接矩阵表示G
inti;
for(i=0;in;i++)
visited[i]=0;//标志向量初始化
for(i=0;in;i++)
if(!
visited[i])//vi未访问过
DFSM(G,i);//以vi为源点开始DFSM搜索
}
邻接表表示的深度优先搜索算法
intvisited[VertexNum]={0};//定义标志向量
voidDFS(ALGraph*G,inti)
{//以vi为出发点进行深度优先搜索
EdgeNode*p;
printf("%4c",G->adjlist[i].vertex);//访问顶点vi
visited[i]=1;//标记vi已访问
p=G->adjlist[i].firstedge;//取vi边表的头指针
while(p){//依次搜索vi的邻接点vj,这里j=p->adjvex
if(!
visited[p->adjvex])//若vi尚未被访问
DFS(G,p->adjvex);//则以vj为出发点向纵深搜索
p=p->next;//找vi的下一邻接点
}
}
voidDFSTraverse(ALGraph*G)
{//深度优先遍历以邻接表表示G
inti;
for(i=0;in;i++)
visited[i]=0;//标志向量初始化
for(i=0;in;i++)
if(!
visited[i])//vi未访问过
DFS(G,i);//以vi为源点开始DFS搜索
}
3、图的广度优先遍历
设图G的初态是所有顶点均未访问过。
在G中任选一顶点v为源点,则广度优先遍历可以定义为:
①递归定义:
首先访问出发点v,接着依次访问v的所有邻接点w1,w2,…,wt,然后再依次访问与wl,w2,…,wt邻接的所有未曾访问过的顶点。
依此类推,直至图中所有和源点v有路径相通的顶点都已访问到为止。
此时从v开始的搜索过程结束。
若G是连通图,则遍历完成;否则,在图G中另选一个尚未访问的顶点作为新源点继续上述的搜索过程,直至G中所有顶点均已被访问为止。
Ø说明:
广度优先遍历类似于树的按层次遍历。
采用的搜索方法的特点是尽可能先对横向进行搜索,故称其为广度优先搜索(Breadth-FirstSearch)。
相应的遍历也就自然地称为广度优先遍历。
②广度优先搜索的过程
在广度优先搜索过程中,设x和y是两个相继要被访问的未访问过的顶点。
它们的邻接点分别记为x1,x2,…,xs和y1,y2,…,yt。
为确保先访问的顶点其邻接点亦先被访问,在搜索过程中使用FIFO队列来保存已访问过的顶点。
当访问x和y时,这两个顶点相继入队。
此后,当x和y相继出队时,我们分别从x和y出发搜索其邻接点x1,x2,…,xs和y1,y2,…,yt,对其中未访者进行访问并将其入队。
这种方法是将每个已访问的顶点入队,故保证了每个顶点至多只有一次入队。
③广度优先遍历的递归算法(BFS算法)
邻接矩阵表示的广度优先遍历算法:
intvisited[VertexNum]={0};//定义标志向量
voidBFSM(MGraph*G,intk){//以vk为源点对用邻接矩阵表示的图G进行广度优先搜索
inti,j;
CirQueueQ;
InitQueue(&Q);
printf("%4c",G->vexs[k]);//访问源点vk
visited[k]=1;
EnQueue(&Q,k);
while(!
QueueEmpty(&Q)){
i=DeQueue(&Q);//vi出队
for(j=0;jn;j++)//依次搜索vi的邻接点vj
if(G->edges[i][j]==1&&!
visited[j]){//vi未访问
printf("%4c",G->vexs[j]);//访问vi
visited[j]=1;
EnQueue(&Q,j);//访问过的vi入队
}
}
}
voidDFSTraverse(MGraph*G){//广度优先遍历以邻接矩阵表示G
inti;
for(i=0;in;i++)
visited[i]=0;//标志向量初始化
for(i=0;in;i++)
if(!
visited[i])//vi未访问过
BFSM(G,i);//以vi为源点开始DFSM搜索
}
邻接表表示的广度优先遍历算法:
intvisited[VertexNum]={0};//定义标志向量
voidBFS(ALGraph*G,intk)
{//以vk为源点对用邻接表表示的图G进行广度优先搜索
inti;
CirQueueQ;//须将队列定义中DataType改为int
EdgeNode*p;
InitQueue(&Q);//队列初始化
//访问源点vk
printf("%4c",G->adjlist[k].vertex);
visited[k]=1;
EnQueue(&Q,k);//vk已访问,将其入队。
(实际上是将其序号入队)
while(!
QueueEmpty(&Q)){//队非空则执行
i=DeQueue(&Q);//相当于vi出队
p=G->