图的遍历
从图中的某个顶点出发,按某种方法对图中的所有顶点访问且仅访问一次。
① 为每个顶点设一个访问标志,用以标示是否被访问过;
② 访问标志用数组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);}