关于图的算法
- Dargon
- 2021/01/24
- 所遇到的的重要的问题: 加深自己能理解 或者记住自己现阶段对于
图
的算法理解 - 教科书 来自:《大话数据结构》第七章 图
01 深度优先遍历(Deepth_First_Search)
- 事实上 深度优先遍历就是一个,深度优先函数进行递归的过程,他从图中某个顶点Vertex出发,访问此顶点,然后从v的未被访问的邻接点出发进行 开始深度优先遍历图,直到图中所有的和V有路径相通的顶点都被访问到。
- 主要感觉 当递归返回上一层这种感觉时候,可以处理掉上一层的点,就能解决一些很大问题。
1.1 邻接矩阵的实现
- 关于代码:
void DFS_matrix( GraphMatrix G, int i ) {
int j;
visited[i] =TRUE;
printf("%c", G.vexs[i]);
for( j =0; j <G.numVertexes; j++ ) {
if( G.arc[i][j] ==TRUE && !visited[j] ) {
DFS_matrix( G, j );
}
}
}
void DFS_matrix_traverse( GraphMatrix G ) {
int i;
for( i =0; i <G.numVertexes; i++ ) {
visited[i] =FALSE;
}
for( i =0; i <G.numVertexes; i++ ) {
if( !visited[i] ) {
DFS_matrix( G, i );
}
}
}
- 对于一个图,找一个顶点,按照一个规定方向,递归开始,比如在迷宫问题中 从右边开始走,到一个顶点,当作起始点,再次递归进行(对于其他的,没有查看到的顶点,在递归返回到上一层后 会顺带解决它们)。
- 所以对于一个图来说,如果顶点都是联通的话,从一个顶点开始,直接通过递归就可以将所有的顶点都能访问到。
1.2 邻接表的实现
- 关于代码:
- 理解:
对于访问的顶点,利用递归调用的时候,使用链表指针代替for循环。
02 广度优先遍历(Breadth_First_Search)
- 图的深度优先搜索类似树的前序遍历, 则广度优先遍历类似于 层序遍历,大概理解 我走的不深 不是一头扎下去走到最后,而是一层一层的去访问(Vertex)节点。
2.1 邻接矩阵的实现
- 关于代码:
void BFS_matrix_traverse( GraphMatrix G ) {
int i, j;
QueueNode Q;
for( i =0; i <G.numVertexes; i++ ) {
visited[i] =FALSE;
}
queue_init( &Q );
for( i =0; i <G.numVertexes; i++ ) {
if( !visited[i] ) {
visited[i] =TRUE;
printf("%c", G.vexs[i]);
queue_add( &Q, i );
while( !queue_empty( Q ) ) {
queue_delete( &Q, &i );
for( j =0; j <G.numVertexes; j ++ ) {
if( G.arc[i][j] ==1 && !visited[j] ) {
visited[j] =TRUE;
printf("%c", G.vexs[j]);
queue_add( &Q, j );
}
}
}
}
}
}
- 从一个顶点(Vertex)开始,将此顶点入队列,在while()循环中,再将顶点弹出队列,保留下标值,然后找与此顶点所链接的顶点 分别操作 标记为已访问,然后进入队列。
- 再次进入while() 循环,将第一个连接点 弹出队列,标记访问其连接点 并进行入队。
- 这就基本形成 一层一层 进行访问的效果, 层序遍历。
2.2 邻接表的实现
- 关于代码:
- 理解:
将while()里面的循环,将与顶点(Vertex)的连接点,变成指针链表去寻找下一个节点,不是依靠矩阵去搜寻。
03 最小生成树(Minimum Cost Spanning Tree)
- 将一颗具有(n)个顶点Vertex的图,生成一个具有(n-1)条边的树,且在边中具有权值的时候,将所有的权值综合尽量的小 就是所谓的最小生成树。
3.1 普利姆算法(Prim Algorithm)
- 关于代码:
void mini_span_tree_prim(GraphMatrix G) {
int min, i, j, k;
int adjvex[MAXVEX];
int lowcost[MAXVEX];
lowcost[0] =0;
adjvex[0] =0;
for( i =1; i <G.numVertexes; i++ ) {
lowcost[i] =G.arc[0][i];
adjvex[i] =0;
}
for( i =1; i <G.numVertexes; i++ ) {
min =INFI;
j =1;
k =0;
while( j <G.numVertexes ) {
if( lowcost[j] !=0 && lowcost[j] <min ) {
min =lowcost[j];
k =j;
}
j ++;
}
printf("(%d,%d)", adjvex[k], k);
lowcost[k] =0;
for( j =1; j <G.numVertexes; j++ ) {
if( lowcost[j] !=0 && G.arc[k][j] <lowcost[j] ) {
lowcost[j] =G.arc[k][j];
adjvex[j] =k;
}
}
}
}
- 在初始化中,声明两个数组lowcost[] 和adjvex[], 数组lowcost[] 初始化为与V0 邻接的矩阵的值,事后将后面的小值更新到此数组里面进来。adjvex[] 数组初始化用来记录下标。
- 在lowcost[] 数组里面找到最小值,记录下标,然后将此下标对应的lowcost[] 里的值变成 0(没有特殊意义 表示此节点已经是最小值 不用更改了),在利用与此下标所连接的边的权值,与lowcost[] 数组里面对应的值进行比较,用两者较小的值去更新数组。
- 再次循环找 数组里面 最小的值,记录下标,将对应下标的lowcost[]元素 进行清零,更新数组。
- 最终会将lowcost[] 里面基本都变成小值,算法执行的顺序就是按照 在lowcost[] 里面的非0 的最小值,去进行一次次的循环 ,并用新的 较小值更新数组。
- Adjvex[] 元素的每一步的生成顺序,记录着最小树的生成过程。
3.2 克鲁斯卡尔算法(Kruskal Algorithm)
- Prime算法是从顶点的角度考虑,而Kruskal算法则从 边的角度去考虑
- 代码如下:
int kruskal_find( int *parent, int f ) {
while( parent[f] >0 ) {
f =parent[f];
}
return f;
}
void mini_span_tree_kruskal( GraphMatrix G ) {
int i, j, n, m;
int flag =0;
EdgeList edges[MAXVEX];
EdgeList temp;
int parent[MAXVEX];
for( i =0; i <G.numVertexes-1; i++ ) {
for( j =i +1; j <G.numVertexes; j++ ) {
if( G.arc[i][j] <INFI ) {
edges[i].begin =i;
edges[i].end =j;
edges[i].weight =G.arc[i][j];
}
}
}
for( i =G.numEdges -1; i >=0; i-- ) {
flag =0;
for(j =0; j <i; j++) {
if( edges[j].weight >edges[j +1].weight ) {
temp =edges[j];
edges[j] =edges[j +1];
edges[j +1] =temp;
flag =1;
}
}
if( flag ==0 ) break;
}
for( i =0; i <G.numVertexes; i++ ) {
parent[i] =0;
}
for( i =0; i <G.numEdges; i++ ) {
n =kruskal_find( parent, edges[i].begin );
m =kruskal_find( parent, edges[i].end );
if( n !=m ) {
parent[n] =m;
printf("(%d, %d) %d", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
- 将邻接矩阵转化为邻接表的形式,并且按照权重值进行从小到大的顺序进行排列。
- 整体是按照顺序去连接,注意 此时不能形成环,即是(n != m),对于此处的判断,应该是 对于已连接的点,从begin 找到 end查看值 若相同 就是已经形成环了 就是封闭了,若没有 则加入生成树中。
- 整体来说 边数少的时候 很有效。
04 最短路径
- 从源点到目标点的所经过的边的权值和 是最小的一条路 和最小生成树还是有些区别的
4.1 迪杰斯特拉算法(Dijkstra Algorithm)
- 关于代码:
void shortest_path_dijkstra( GraphMatrix G, int v0, Patharc *P, ShortPathTable *D ) {
int v, w, k, min;
int final[MAXVEX];
for( v =0; v <G.numVertexes; v++ ) {
final[v] =0;
(*D)[v] =G.arc[v0][v];
(*P)[v] =0;
}
(*D)[v0] =0;
final[v0] =1;
for( v =1; v <G.numVertexes; v++ ) {
min =INFI;
for( w =0; w <G.numVertexes; w++ ) {
if( !final[w] && (*D)[w] <min ) {
k =w;
min =(*D)[w];
}
}
final[k] =1;
for( w =0; w <G.numVertexes; w++ ) {
if( (!final[w]) && ( min +G.arc[k][w] <(*D)[w] ) ) {
(*D)[w] =min +G.arc[k][w];
(*P)[w] =k;
}
}
}
}
- p[] 数组是存储对应最短路径下标 D[] 数组用于存储源点到各点最短路径的权值和 final[] 初始化为0 用来表示未知的状态。
- 将D[] 初始化成为v0 的邻接矩阵,找到D 数组中最小值,相当于在与v0 所连接的点中,找到距离最小的,将对应的final数组里的元素置1,然后根据这个找到的点(相当于该点作为前驱)找与该点连接的顶点 并且根据较短距离原则来更新D数组。
- 作为前驱点记录下标 更新记录在P 数组里面
- 下面每一步 都是在前面找到最短的基础上再来寻找最小值的。
- 最终 D[] 数组里面的内容表示v0 点到各个顶点的最短路径,注意 每个元素 都是路径和,P[] 相当于每个路径都是作为前驱节点存在的,例如P[0 0 1 4 2 4 3 6 7] P[8] =7表示顶点V8的前驱节点是V7(V8 是从 V7那里过来的),再找P[7] =6 就是V7的前驱是V6节点,再找p[6] =3 就是V6的前驱是V3节点,逐步求之 ,可以得到整个路径。
4.2 弗洛伊德算法(Floyd Algorithm)
- 关于代码:
void shortest_path_floyd( GraphMatrix G, PathMatrix *P, ShortPath *D ) {
int v, w, k;
for( v =0; v <G.numVertexes; ++v ) {
for( w =0; w <G.numVertexes; w++ ) {
( *D )[v][w] =G.arc[v][w];
( *P )[v][w] =w;
}
}
for( k =0; k <G.numVertexes; k++ ) {
for( v =0; v <G.numVertexes; v++ ) {
for( w =0; w <G.numVertexes; w++ ) {
if( ( *D )[v][w] > ( *D )[v][k] +( *D )[k][w] ) {
( *D )[v][w] =( *D )[v][k] +( *D )[k][w];
( *P )[v][w] =( *P )[v][k];
}
}
}
}
for( v =0; v <G.numVertexes; v++ ) {
for( w =v +1; w <G.numVertexes; w++ ) {
printf("v%d-v%d weight: %d", v, w, ( *D )[v][w]);
k =( *P )[v][w];
printf("Path : %d", v);
while( k !=w ) {
printf("-> %d", k);
k =( *P )[k][w];
}
printf("-> %d", w);
}
printf("\n");
}
}
- 基本思想也是通过找最小路径的问题 很巧妙通过 中间点的转换 进行更新邻接矩阵 例如开始的时候 是从V0–>V2,为5 的路径,若是从V0–>V1–>V2,距离为3 则需要替代。
- 相当于将
Dijkstra Algorithm
进行升级 成从任意点到任意点的最小路径,当然前面算法的D数组和 P数组自然就变成二维数组 进行路径和权重的记录。 - 通过中间点找到更短值的就进行替换。最初的
D
−
1
D^{-1}
D−1,经过一轮轮的跟新到
D
0
,
D
1
,
D
2
…
…
D^{0},D^{1},D^{2} ……
D0,D1,D2……
05 关于AOV AOE网
- AOV (Activity On Vertex Network) 分别表示活动网 有活动的先后顺序,对于边(弧)则代表着一种制约关系,具有顺序。
- AOE (Activity On Edge Network) 相当于弧长值 带有活动进行的时间的活动网。
5.1 拓扑排序
- 关于代码:
int topo_logical_sort( GraphList *G ) {
EdgeNode *current;
int i, k, gettop;
int top =0;
int count =0;
int *stack;
stack =(int *)malloc( sizeof(int) * G->numVertexes );
for( i =0; i <G->numVertexes; i++ ) {
if( G->adjlist[i].in ==0 ) {
stack[++top] =i;
}
}
while( top !=0 ) {
gettop =stack[top--];
printf("%d-> ", G->adjlist[gettop].data);
count ++;
for( current =G->adjlist[gettop].firstedge; current; current =current->next ) {
k =current->adjvex;
if( !( -- (G->adjlist[k].in) ) ) {
stack[++ top] =k;
}
}
}
if( count <G->numVertexes )
return ERROR;
else
return TRUE;
}
- 先转化到邻接表(顶点带有入度)的形式,进行循环 将所有入度为0 的顶点进行堆栈 PUSH操作,然后进行POP操作,将POP出的点 将与之所相连的顶点的入度
-1
,判断入度是否为0,是PUSH。(这就相当于 需要把本点前面的事情都做完 之后,才能来到这个节点 进行操作),相当于将 入度为0 的。
5.2 关键路径
- 应该是难理解的一个算法了
- 拓扑排序讲的是解决一个工程能否顺利进行的问题,而这里我们还需要关心 总的完成时间问题。理解为最关键的问题,每个把步骤最长的时间,就很紧密 中间无休息的那种,成为关键步骤。把具有最大长度的路径(时间),才是我们要找的关键路径。
- 理解几个参数etv(earliest time of vertex) 事件的最早发生时间 ==>ltv;
- 还有ete(earliest time of edge) 活动最早开始时间 ==> lte
- 求etv的代码:
#define ERROR 0
int *etv, *ltv;
int *stack2;
int top2;
int topo_logical_sort_v2( GraphList *G ) {
EdgeNode *current;
int i, k, gettop;
int top =0;
int count =0;
int *stack;
stack =(int *)malloc( sizeof(int) * G->numVertexes );
for( i =0; i <G->numVertexes; i++ ) {
if( G->adjlist[i].in ==0 ) {
stack[++top] =i;
}
}
top2 =0;
etv =(int *)malloc( G->numVertexes*sizeof(int) );
for( i =0; i <G->numVertexes; i++ ) {
etv[i] =0;
}
stack2 =(int *)malloc( G->numVertexes *sizeof(int) );
while( top !=0 ) {
gettop =stack[top--];
count ++;
stack2[++top2] =gettop;
for( current =G->adjlist[gettop].firstedge; current; current =current->next ) {
k =current->adjvex;
if( !( -- (G->adjlist[k].in) ) ) {
stack[++ top] =k;
}
if( etv[gettop] +current->weight >etv[k] ) {
etv[k] =etv[gettop] +current->weight;
}
}
}
if( count <G->numVertexes )
return ERROR;
else
return TRUE;
}
- 求关键路径代码:
void critical_path( GraphList *G ) {
EdgeNode *e;
int i, j, k, gettop;
int ete, lte;
topo_logical_sort_v2( G );
ltv =(int *)malloc( G->numVertexes *sizeof(int) );
for( i =0; i <G->numVertexes; i++ ) {
ltv[i] =etv[G->numVertexes -1];
}
while( top2 !=0 ) {
gettop =stack2[top2--];
for( e =G->adjlist[gettop].firstedge; e; e->next ) {
k =e->adjvex;
if( ltv[k] -e->weight <ltv[gettop] ) {
ltv[gettop] =ltv[k] -e->weight;
}
}
}
for( j =0; j <G->numVertexes; j++ ) {
for( e =G->adjlist[j].firstedge; e; e=e->next ) {
k =e->adjvex;
ete =etv[j];
lte =ltv[k] -e->weight;
if( ete ==lte ) {
printf("<v%d,v%d> length: %d , ", G->adjlist[j].data, G->adjlist[k].data, e->weight);
}
}
}
}
- 更新etv[] 就是找出最长(最耗时的步骤)作为最早发生时间,就是从源点到该点所走路径达到的最大时间,就是从V0在走其它路径到这儿的话,用的时间都是 <= 最大时间。
- 同时在更新ltv[] 时候 ,对应于另一件事情 (<=最大时间的) 最短的时间 min(用最长时间 - 边的权重值(活动时间))
- 举个例子: etv[1] =3 ,ltv[1] =7 ,v1 这个时间最早也只能在
第三天
开始(按照总的工程进度来说话),同时 v1也可以在第七天
开始 也可以不影响工期的完成。说明中间有四天的灵活时间
,(像你是先写作业 还是先玩是一样的事情),如果灵活时间
没有 ,则此时就是关键的路径了 时间紧凑的感觉。 - 活动的最早发生时间ete 就像当于 etv(事件的最早发生时间),活动的最迟发生时间lte[] 就是求最小的发生时间 相当于 最迟的发生时间(你这条路线再不去做的话,就是你耽误工程时间)(这一段 还需要细细理解 细细去品 !!!)
总结
- 第二遍去学习这个算法,然而第一遍当时挺明白,在第二遍的时候,又有重头再来的感觉,逻辑重新整理,又再次进入两天出来,所以这次要把学到的,看到的,记录下来 记录自己现在所看到的东西,于是用一天的时间来记录。
- 暂且把这当做过程吧! 慢啊慢啊 这应该就是学习。