图的遍历与应用

图的遍历

从图中的某个顶点出发,按某种方法对图中的所有顶点访问且仅访问一次。
① 为每个顶点设一个访问标志,用以标示是否被访问过;
② 访问标志用数组visited[n]来表示 。

深度优先搜索:

是指按照深度方向搜索 ,它类似于树的先根遍历

基本思想:
1)从图中某个顶点v0出发,访问v0。
2)找出刚访问顶点vi的第一个未被访问的邻接点,访问该顶点。重复此步骤,直到当前的顶点没有未被访问的邻接点为止。
3)返回前一个访问过的顶点,找出其下一个未被访问的邻接点,访问之;转2)。

过程示例:
A为起始顶点,实箭头代表访问方向,虚箭头代表回溯方向,箭头旁数字代表搜索顺序。
访问序列为:A、B、C、F、E、G、D、H、I。

深度优先搜索的算法:

#define True 1
#define False 0
#define Error 1   /*出错*/
#define Ok 1
int visited[MAX_VERTEX_NUM];   /*访问标志数组*/ 
void TraverseGraph (Graph g) 
{
	for(vi = 0; vi < g.vexnum; vi++)  visited[vi] = False;/*标志数组初始化*/
	for(vi = 0; vi < g.vexnum; vi++)	/*每顶点循环一次*/
	 if (!visited[vi])  DepthFirstSearch(g,vi)/*连通图仅循环一次*/
}/* TraverseGraph */ 

深度遍历v0所在的连通子图:

void  DepthFirstSearch(Graph g, int v0) 
{
	visit(v0);visited[v0] =True;/*访问v0,并置访问标志数组*/
	w = FirstAdjVertex(g, v0);
	while ( w != -1) 
	{/*邻接点存在*/
		if(!visited[w])  DepthFirstSearch(g, w);  /*递归调用*/
   		w = NextAdjVertex(g, v0, w); /*找下一个邻接点*/
  	}
} /*DepthFirstSearch*/ 

用邻接矩阵方式实现深度优先搜索:

void DepthFirstSearch(AdjMatrix g, int v0)   
{
	visit(v0);
	visited[v0] = True;
	for(vj = 0; vj < g.vexnum; vj++)
		if(!visited[vj] && g.arcs[v0][vj].adj == 1) 
			DepthFirstSearch(g, vj);
}

用邻接表方式实现深度优先搜索:

void DepthFirstSearch(AdjList g, int v0)   
{	visit(v0);
	visited[v0] = True;
	p = g.vertex[v0].firstarc;//v0边表头,指向第一邻接点
	while(p != NULL)
	{	if(!visited[p->adjvex])
			DepthFirstSearch(g, p->adjvex);
		p = p->nextarc;
	}
}

以邻接表作为存储结构,查找每个顶点的邻接点的时间复杂度为O(e), 其中e是无向图中的边数或有向图中弧数, 则深度优先搜索图的时间复杂度为O(n+e)。

用非递归过程实现深度优先搜索:

每次循环弹出一顶点进行访问,压入其所有邻接点;
下次弹出最后一个邻接点访问,再压入所有邻接点。

void DepthFirstSearch(Graph g, int v0) 
{  /*从v0出发深度优先搜索图g*/
InitStack(&S);  /*初始化空栈*/       
Push(&S, v0)while ( ! Is_Empty(S))
  { Pop(&S, &v);  
	if (!visited(v)) 
	  { /*栈中可能有重复结点*/
	     visit(v);  
	     visited[v]=True; 
	     w = FirstAdj(g, v);  /*求v的第一个邻接点*/
	     while (w != -1 )
	     { if (!visited(w))  Push(&S, w);
		    w = NextAdj(g, v, w);  /*求v相对于w的下一个邻接点*/ }
	  }
  }
} 

广度优先搜索:

按照广度方向搜索,它类似于树的按层次遍历。
基本思想:类似声波、水波
1)从图中某个顶点v0出发,首先访问v0;
2)依次访问v0的各个未被访问的邻接点;
3)分别从这些邻接点(端结点)出发,依次访问它们的各个未被访问的邻接点(新的端结点);
访问时应保证:
如果Vi和Vk为当前端结点,且Vi在Vk之前被访问,则Vi的所有未被访问的邻接点应在Vk的所有未被访问的邻接点之前访问。

4)重复(3),直到所有端结点均没有未被访问的邻接点为止;
5)若此时还有顶点未被访问,则选一个未被访问的顶点作为起始点,重复上述过程,直至所有顶点均被访问过为止。

搜索过程示例:
箭头代表搜索方向,箭头旁边的数字代表搜索顺序,A为起始顶点。
访问序列为:A、B、E、D、C、G、F、H、I。

广度优先搜索连通子图的算法:

void  BreadthFirstSearch(Graph g, int v0)
{
    visit(v0); visited[v0] = True;
    InitQueue(&Q);  
    EnterQueue(&Q, v0)/* v0进队*/
    while ( ! Empty(Q))
     {  DeleteQueue(&Q, &v);  /*队头元素出队*/
	    w = FirstAdj(g, v);  /*求v的第一个邻接点*/
	    while (w != -1 )
	    { if (!visited(w))  
	        { visit(w); visited[w] = True;
        	  EnterQueue(&Q, w); 
        	}
 		   w = NextAdj(g, v, w);  /*求下一个邻接点*/
 		}     
 	}  
} 

图的应用

图的连通性问题:

1、无向图的连通分量
对于连通图:
仅需要调用一次搜索过程,即从任一个顶点出发,便可以遍历图中的各个顶点。
对于非连通图:
需要多次调用搜索过程,而每次调用得到的顶点访问序列恰为各连通分量中的顶点集。

调用搜索过程的次数就是该图连通分量的个数。

2、两顶点间的简单路径
常需找一顶点u到另一顶点v的简单路径。要求:
① 顶点均不同(无环路);
② 简单路径可能多条,跟选择算法相关。

算法思想:(遍历实质就是探路过程)
① 从顶点u开始,进行深度(或广度)优先搜索,如果能搜索到顶点v,则表明顶点u到v有一条简单路径;
② 搜索同时记录线路,既得路径。
设置数组:pre[n]
记录顶点遍历前驱,vi->vj,pre[j]=i,搜索完后从pre追溯路径
pre代替visited数组,初始pre[n]={-1},pre[j]记录了前驱表示已被访问

int *pre;
void one_path(Graph *G, int u, int v)
//找一条从第u个顶点到第v个顶点的简单路径
{	int i;
	pre = (int *)malloc(G->vexnum*sizeof(int));
	for(i = 0; i < G->vexnum; i++)	pre[i] = -1;    	
	pre[u] = -2;      /*表示第u个顶点已被访问,且无前驱*/
    DFS_path(G, u, v);    /*用深度优先搜索从u到v的简单路径。*/    	
    free(pre);
}

int DFS_path(Graph *G, int u, int v)
{  /*深度优先u到v*/
    int j;
    for(j = firstadj(G,u); j >= 0; j = nextadj(G, u, j))
	   if(pre[j] == -1)//j未被访问
	   {	pre[j] = u;//标记j前驱为u
            if(j == v)
            {  print_path(pre, v); 
               return 1;
            }
            else if(DFS_path(G, j, v)) return1;//以j为新起点继续 
	   }
    return 0;
}

3、图的生成树
生成树:
一个连通图的生成树是指一个极小连通子图,它含有图的全部顶点,但只有足以构成树的n-1条边。任添加一条边必构成一个环。
最小生成树:
一个连通网的所有生成树中,各边的代价之和最小的那棵生成树。
MST性质:
设N=(V,{E}) 是一连通网,U 是顶点集V的一个非空子集。若(u , v)是一条具有最小权值的边,其中u∈U,v∈V-U,则存在一棵包含边(u , v)的最小生成树。

最小生成树算法:

(1) 普里姆算法——加点法

假设N=(V,{E})是连通网,TE为最小生成树中边的集合。
1)初始U={u0}(u0∈V), TE=φ;
2)在所有u∈U, v∈V-U的边中选一条代价最小的边(u,v)并入集合TE,同时将v并入U;
3)重复(2),直到U=V为止。
此时,TE中必含有n-1条边,则T=(V,{TE})为N的最小生成树。
在这里插入图片描述
注意:选择最小边时可能有多条同样权值的边,任选其一。
算法需设一辅助数组closedge[ ],以记录从U到V-U具有最小代价的边。

算法思想:
① 将初始顶点u加入U,其余顶点i,closedge[i]均初始化为i到u的边信息;
② 循环n-1次:
a:从各组最小边closedge[]中选最小的closedge[v],将v加入U中;
b:更新剩余的每组最小边信息closedge[i].

struct
{ 
	VertexData adjvex;  //当前顶点到U集中顶点的邻接点
    int lowcost;   //当前顶点到U集中顶点最小代价
} closedge[MAX_VERTEX_NUM];  //辅助数组

MiniSpanTree_Prim(AdjMatrix gn, int u) 
{/*从顶点u出发*/
    closedge[u].lowcost = 0;   /*初始化,U={u},U集中顶点lowcost为0 */
    for (i = 0; i < gn.vexnum; i++) /*对V-U中的顶点i,初始化closedge[i]*/
  	   if ( i != u) 
  	   {  closedge[i].adjvex = u; 
  	      closedge[i].lowcost = gn.arcs[u][i].adj;
  	   }
    for (e = 1; e <= gn.vexnum - 1; e++) 
    {  /*找n-1条边(n= gn.vexnum) */
	    v = Minium(closedge);     /* closedge[v]存当前最小边(u, v)的信息*/
	    u = closedge[v].adjvex;   	/* u∈U*/
       	printf(u, v);  closedge[v].lowcost = 0;     /*将顶点v0纳入U集合*/
       	for (i = 0; i < vexnum; i++)    /*v并入U后更新closedge[i]*/
           if (gn.arcs[v][i].adj < closedge[i].lowcost)
             {
            	closedge[i].lowcost = gn.arcs[v][i].adj;        
            	closedge[i].adjvex = v;
             }
    }
}

(2)克鲁斯卡尔算法——加边法:

假设N = (V, {E})是连通网,将其边按权值从小到大排序;
1)将n个顶点看成n个集合;
2)按权值由小到大的顺序选择边,所选边应满足两个顶点不在同一个顶点集合内,将该边放到生成树边的集合中。同时将该边的两个顶点所在的顶点集合合并;
3)重复2),直到所有的顶点都在同一个顶点集合内。

在这里插入图片描述

稠密图——普里姆算法;
稀疏图——克鲁斯卡尔算法。

有向无环图的应用:

可用来描述工程或系统的进行过程,施工图、课程间制约关系等。

1、拓扑排序

AOV-网:顶点表示活动的网;
① 用顶点表示活动;
② 用弧表示活动间的优先关系的有向无环图 。
拓扑序列:
有向图G = (V,{E})的顶点线性序列(vi1,vi1,vi3,…,vin) ,必须满足前驱后继关系。

AOV-网的特性:
① 先行关系具有可传递性;
② 拓扑序列不是唯一的。

求拓扑排序的基本思想:
1、从有向图中选一个无前驱的顶点输出;
2、将此顶点和以它为起点的弧删除;
3、重复1、2直到不存在无前驱的顶点;
4、若此时输出的顶点数小于有向图中的顶点数,则说明有向图中存在回路,否则输出的顶点的顺序即为一个拓扑序列。

1、基于邻接矩阵表示的存储结构:

算法步骤:
对邻接矩阵的列进行排序编号:
1)取1作为第一新序号;
2)找一个未新编号的、值全为0的列j,若找到,则转3);
否则,若所有的列全部都编过号,拓扑排序结束; 若有列未曾被编号,则该图中有回路;
3)输出列号对应的顶点j,把新序号赋给所找到的列;
4)将矩阵中j对应的行全部置为0;
5)新序号加1,转2);

在这里插入图片描述
拓扑序列为:v1,v6,v4,v3,v2,v5或v1,v3,v2,v6,v4,v5.

2、基于邻接表的存储结构

在这里插入图片描述

附设一个存放各顶点入度的数组indegree[ ],于是有 :
1)找G中无前驱的顶点——查找indegree[i]为零的顶点vi;
2)删除以i为起点的所有弧——对链在顶点i后面的所有邻接顶点k,将对应的indegree[k]减1。

为避免重复检测入度为零的顶点,可再设置一个辅助栈:
若某一顶点的入度减为0,则将它入栈。每当输出某一顶点时,便将它从栈中删除。

算法思想:
1、首先求各顶点入度,并将入度为0的顶点入栈;
2、只要栈不空,则重复下面处理:
将栈顶顶点i出栈并打印;
将顶点i的每一个邻接点k的入度减1,如果顶点k的入度变为0,则将顶点k入栈;

算法只修改indegree[]和堆栈,不修改邻接表。

拓扑排序算法:

int TopoSort (AdjList G)
{   Stack S; int indegree[MAX_VERTEX_NUM];    
    int i, count, k;       
    ArcNode *p;
    FindID(G, indegree);  /*求各顶点入度*/      
    InitStack(&S);       /*初始化辅助栈*/
    for(i = 0; i < G.vexnum; i++)       
        if(indegree[i] == 0) Push(&S, i);    /*将入度为0的顶点入栈*/
    count = 0;
    while(!StackEmpty(S))
     {  Pop(&S, &i); 	   
        printf("%c", G.vertex[i].data);
	    count++;  /*输出i号顶点并计数*/            p = G.vertexes[i].firstarc;//访问其链表(作为起点的弧)
	    while(p != NULL)
	    {//更新i邻接点的入度
	        k = p->adjvex;             
	       indegree[k]--;      /*i号顶点的每个邻接点的入度减1*/
	       if(indegree[k] == 0)  Push(&S, k);  /*若入度减为0,则入栈*/
	       p = p->nextarc;     
	     }
	  } /*while*/
     if (count < G.vexnum)  return(Error);  /*该有向图含有回路*/
     else  return(Ok);
} 

求顶点入度算法:

void FindID( AdjList G, int indegree[MAX_VERTEX_NUM])
{   int i; ArcNode *p;
    for(i = 0; i < G.vexnum; i++)     
       indegree[i] = 0;  //初始化indegree
    for(i = 0; i < G.vexnum; i++)
    {  p = G.vertexes[i].firstarc;//获取每个顶点的边链表
	   while(p != NULL)
	   {//扫描每个顶点的边链表
	   	   indegree[p->adjvex]++;//p指向的弧入度累加到p->adjex
	       p = p->nextarc;    
	   }     
	}  
} 

用拓扑排序算法求的拓扑序列为:v6,v1,v3,v2,v4,v5。
时间复杂度为O(n+e)。

2、关键路径

AOE-网:
在有向图中,用顶点表示事件,用弧表示活动,弧的权值表示活动所需要的时间。这种方法构造的有向无环图叫做AOE-网。
源点
存在唯一的、入度为零的顶点——起点。
汇点
存在唯一的、出度为零的顶点——终点。
关键路径
从源点到汇点的最长路径的长度即为完成整个工程任务所需的时间,该路径叫做关键路径。
关键活动:
关键路径上的活动叫做关键活动。

事件vi的最早发生时间ve(i):
从源点到顶点vi的最长路径的长度,叫做事件vi的最早发生时间。要保证前序事件和活动全部完成。
求ve(i) 时可从源点开始,按拓扑顺序向汇点递推。

事件vi的最晚发生时间vl(i):
在保证汇点按其最早发生时间发生这一前提下,事件vi的最晚发生时间。
在求出ve(i)的基础上,从汇点开始,按逆拓扑顺序向源点递推,求出vl(i)。

活动ai的最早开始时间e(i):
如果活动ai对应的弧为<j,k>,则e(i)等于从源点到顶点j的最长路径的长度,即:e(i)=ve(j) ——弧起点ve。

活动ai的最晚开始时间l(i):
如果活动ai对应的弧为<j,k>,其持续时间为dut(<j,k>)则有:l(i)=vl(k)- dut(<j,k>) ——弧终点vl-dut。

活动ai的松弛时间(时间余量):
ai的最晚开始时间与ai的最早开始时间之差:l(i)- e(i)。
显然,松弛时间(时间余量)为0的活动为关键活动。

求关键路径的基本步骤:
1、对图中顶点进行拓扑排序,在排序过程中按拓扑序列求出每个事件的最早发生时间ve(i);
2、按逆拓扑序列求每个事件的最晚发生时间vl(i);
3、求出每个活动ai的最早开始时间e(i)和最晚发生时间l(i);
4、找出e(i)=l(i) 的活动ai,即为关键活动。

算法思想:
1、求出各顶点的入度,并将入度为0的顶点入栈S;
2、将各顶点的最早发生时间ve[i]初始化为0;
3、只要栈S不空,则重复下面处理:
a. 将栈顶顶点j出栈并压入栈T(生成逆拓扑序列);
b. 将顶点j的每一个邻接点k的入度减1,如果k入度变为0,则将k入栈S;
c. 根据顶点j的最早发生时间ve[j]和弧<j, k>的权值,更新顶点k的最早发生时间ve[k]。

修改后的拓扑排序算法:

int  ve[MAX_VERTEX_NUM];    /*每个顶点的最早发生时间*/
int TopoOrder(AdjList G, Stack *T) { /* T为返回逆拓扑序列的栈,计算ve数组*/
int count, i, j, k;  ArcNode *p;  int indegree[MAX_VERTEX_NUM];  /*各顶点入度数组*/
Stack  S;    InitStack(T);  InitStack(&S);   /*初始化栈T,  S*/
FindID(G,  indegree);  /*求各个顶点的入度*/
for(i = 0; i < G.vexnum; i++)   if(indegree[i] == 0)   Push(&S, i);    count = 0;  
for(i = 0; i < G.vexnum; i++)   ve[i] = 0;   /*初始化最早发生时间*/
while(!StackEmpty(S)){
       Pop(&S, &j);      Push(T, j);      count++;      p = G.vertex[j].firstarc;
       while(p!=NULL){//更新j后继
	k = p->adjvex;     if(--indegree[k] == 0)  Push(&S, k);   /*若顶点的入度减为0,则入栈*/
	if(ve[j] + p->weight > ve[k])  ve[k] = ve[j] + p->weight;//更新k的ve,取最大值
	p = p->nextarc;     }  /*while*/       } /*while*/
        if(count < G.vexnum)   return(Error);       else  return(Ok);}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值