关于图的算法,记录对于图的理解

本文深入探讨图算法核心概念,包括深度优先与广度优先遍历、最小生成树、最短路径算法及AOV/AOE网分析。通过实例解析,帮助读者理解算法原理与应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于图的算法

  • Dargon
  • 2021/01/24
  • 所遇到的的重要的问题: 加深自己能理解 或者记住自己现阶段对于的算法理解
  • 教科书 来自:《大话数据结构》第七章 图

01 深度优先遍历(Deepth_First_Search)

  1. 事实上 深度优先遍历就是一个,深度优先函数进行递归的过程,他从图中某个顶点Vertex出发,访问此顶点,然后从v的未被访问的邻接点出发进行 开始深度优先遍历图,直到图中所有的和V有路径相通的顶点都被访问到。
  2. 主要感觉 当递归返回上一层这种感觉时候,可以处理掉上一层的点,就能解决一些很大问题。

1.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;
    /* 初始化numVertexes 个顶点状态 visited[] 矩阵 */
    for( i =0; i <G.numVertexes; i++ ) {
        visited[i] =FALSE;
    }
    // 对 未访问 过的顶点调用 DFS,若是连通图 只会执行一次 
    for( i =0; i <G.numVertexes; i++ ) {
        if( !visited[i] ) {
            DFS_matrix( G, i );
        }
    }
}
  1. 对于一个图,找一个顶点,按照一个规定方向,递归开始,比如在迷宫问题中 从右边开始走,到一个顶点,当作起始点,再次递归进行(对于其他的,没有查看到的顶点,在递归返回到上一层后 会顺带解决它们)。
  2. 所以对于一个图来说,如果顶点都是联通的话,从一个顶点开始,直接通过递归就可以将所有的顶点都能访问到。

1.2 邻接表的实现

  1. 关于代码:
  2. 理解:
    对于访问的顶点,利用递归调用的时候,使用链表指针代替for循环。

02 广度优先遍历(Breadth_First_Search)

  1. 图的深度优先搜索类似树的前序遍历, 则广度优先遍历类似于 层序遍历,大概理解 我走的不深 不是一头扎下去走到最后,而是一层一层的去访问(Vertex)节点。

2.1 邻接矩阵的实现

  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 ); /* 将队中元素 出列 把值赋予 i */
                for( j =0; j <G.numVertexes; j ++ ) {
                    if( G.arc[i][j] ==1 && !visited[j] ) {
                        visited[j] =TRUE; /* 把与第 i 行邻接的 都去访问 层序遍历 */
                        printf("%c", G.vexs[j]);
                        queue_add( &Q, j );
                    }
                }
            }
        }
    }
}
  1. 从一个顶点(Vertex)开始,将此顶点入队列,在while()循环中,再将顶点弹出队列,保留下标值,然后找与此顶点所链接的顶点 分别操作 标记为已访问,然后进入队列。
  2. 再次进入while() 循环,将第一个连接点 弹出队列,标记访问其连接点 并进行入队。
  3. 这就基本形成 一层一层 进行访问的效果, 层序遍历。

2.2 邻接表的实现

  1. 关于代码:
  2. 理解:
    将while()里面的循环,将与顶点(Vertex)的连接点,变成指针链表去寻找下一个节点,不是依靠矩阵去搜寻。

03 最小生成树(Minimum Cost Spanning Tree)

  1. 将一颗具有(n)个顶点Vertex的图,生成一个具有(n-1)条边的树,且在边中具有权值的时候,将所有的权值综合尽量的小 就是所谓的最小生成树。

3.1 普利姆算法(Prim Algorithm)

  1. 关于代码:
void mini_span_tree_prim(GraphMatrix G) {
    int min, i, j, k;
    int adjvex[MAXVEX];  /* 保存相关顶点下标 */
    int lowcost[MAXVEX]; /* 保存相关顶点间的权值 */

    lowcost[0] =0; /* 初始化第一个权值 为0 */
    adjvex[0] =0;  /* 初始化第一个顶点下标为0 */

    /* 初始化 */
    for( i =1; i <G.numVertexes; i++ ) {
        lowcost[i] =G.arc[0][i]; /* 将V0顶点与之有边的权值存入数组 相当于初始化 读入V0 行 */
        adjvex[i] =0; /* 初始化都为 V0 的下标 */
    }

    /* 开始运行 找最小的代价问题 */
    for( i =1; i <G.numVertexes; i++ ) {
        min =INFI;
        j =1;
        k =0;
        /* 寻找lowcost[] 里面最小值 对应的位置 就是下标 */
        while( j <G.numVertexes ) {
            if( lowcost[j] !=0 && lowcost[j] <min ) {
                min =lowcost[j];
                k =j;
            }
            j ++;
        }
        /* 找到位置 进行连接 当前顶点adjvex[k] 找到与之所相连的 最小权值 k边 */
        printf("(%d,%d)", adjvex[k], k); /* 打印所顶点 邻接边中权值最小的边K */
        lowcost[k] =0; /* 更新节点边的权值 数组 当前节点完成任务 相应的k 边为0 */

        /* 找与k 边所连接的边的值 更新lowcost 同事更新adjvex[] 以便下一次找出这条边的最小值 */
        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; /* 此时 将一轮更新中的对应于上一轮的最小值 下标k 存入adjvex[]中  */
                /* 存下标 主要是方便 输出邻边 */
            }
        }
    }
}
  1. 在初始化中,声明两个数组lowcost[] 和adjvex[], 数组lowcost[] 初始化为与V0 邻接的矩阵的值,事后将后面的小值更新到此数组里面进来。adjvex[] 数组初始化用来记录下标。
  2. 在lowcost[] 数组里面找到最小值,记录下标,然后将此下标对应的lowcost[] 里的值变成 0(没有特殊意义 表示此节点已经是最小值 不用更改了),在利用与此下标所连接的边的权值,与lowcost[] 数组里面对应的值进行比较,用两者较小的值去更新数组。
  3. 再次循环找 数组里面 最小的值,记录下标,将对应下标的lowcost[]元素 进行清零,更新数组。
  4. 最终会将lowcost[] 里面基本都变成小值,算法执行的顺序就是按照 在lowcost[] 里面的非0 的最小值,去进行一次次的循环 ,并用新的 较小值更新数组。
  5. Adjvex[] 元素的每一步的生成顺序,记录着最小树的生成过程。

3.2 克鲁斯卡尔算法(Kruskal Algorithm)

  1. Prime算法是从顶点的角度考虑,而Kruskal算法则从 边的角度去考虑
  2. 代码如下:
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];
    /* 矩阵转化 观察如何加进去 */
    /* 将矩阵读取到edges 里面 */
    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];
            }
        }
    }
    /* 进行 Bubble 排序 */
    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;
    }

    /* 数组 parent 的初始化 */
    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; /* 若有回路生成 则有n =m出现 目的是找到最小的 且不出现环的现象 */
            printf("(%d, %d) %d", edges[i].begin, edges[i].end, edges[i].weight);
        }
    }
}
  1. 将邻接矩阵转化为邻接表的形式,并且按照权重值进行从小到大的顺序进行排列。
  2. 整体是按照顺序去连接,注意 此时不能形成环,即是(n != m),对于此处的判断,应该是 对于已连接的点,从begin 找到 end查看值 若相同 就是已经形成环了 就是封闭了,若没有 则加入生成树中。
  3. 整体来说 边数少的时候 很有效。

04 最短路径

  1. 从源点到目标点的所经过的边的权值和 是最小的一条路 和最小生成树还是有些区别的

4.1 迪杰斯特拉算法(Dijkstra Algorithm)

  1. 关于代码:
/* 算法对应的是只找出V0 顶点 到各顶点所对应的 最短路径 其实 若是需要找每一个顶点 也同样需要 O(N^3)复杂度 */
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]; /* 将和v0有关的连线加上权值 */
        (*P)[v] =0;
    }
    (*D)[v0] =0;
    final[v0] =1;

    /* 开始主循环 每次求v0 到某个v 顶点的最短路径 */
    for( v =1; v <G.numVertexes; v++ ) {
        min =INFI;
        for( w =0; w <G.numVertexes; w++ ) {
            /* 在D 中寻找离 v0最近的点 */
            if( !final[w] && (*D)[w] <min ) {
                k =w;
                min =(*D)[w];
            }
        }
        /* 此循环结束 将找到的点进行处理 */
        final[k] =1; /* 将目前找到的最近的顶点 下标 为1 */

        /* 在新的顶点上 更正D数组的 为寻找距离v0点的最小值 */
        for( w =0; w <G.numVertexes; w++ ) {
            /* 若果经过v 顶点的路 比现在D 数组里面的路径 短的话 */
            if( (!final[w]) && ( min +G.arc[k][w] <(*D)[w] ) ) {
                /* 找到最短路径 进行修改 */
                (*D)[w] =min +G.arc[k][w];
                (*P)[w] =k;
            }
        }
    }
}
  1. p[] 数组是存储对应最短路径下标 D[] 数组用于存储源点到各点最短路径的权值和 final[] 初始化为0 用来表示未知的状态。
  2. 将D[] 初始化成为v0 的邻接矩阵,找到D 数组中最小值,相当于在与v0 所连接的点中,找到距离最小的,将对应的final数组里的元素置1,然后根据这个找到的点(相当于该点作为前驱)找与该点连接的顶点 并且根据较短距离原则来更新D数组。
  3. 作为前驱点记录下标 更新记录在P 数组里面
  4. 下面每一步 都是在前面找到最短的基础上再来寻找最小值的。
  5. 最终 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)

  1. 关于代码:
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;
        }
    } 
    
    /* 开始主要的循环 进行比较 */ 
    /* k 作为中转点 */
    for( k =0; k <G.numVertexes; k++ ) { /* 对于for 循环 说 ++k 的意义何在!!! */
        /* v 作为起点 */
        for( v =0; v <G.numVertexes; v++ ) {
            /* w 作为终点 */
            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]; /* D 矩阵进行更新 */
                    ( *P )[v][w] =( *P )[v][k];   /* 同时 P矩阵也进行更新 */
                }
            }
        }
    }

    /* 最优路径的打印输出 */
    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");
    }
}
  1. 基本思想也是通过找最小路径的问题 很巧妙通过 中间点的转换 进行更新邻接矩阵 例如开始的时候 是从V0–>V2,为5 的路径,若是从V0–>V1–>V2,距离为3 则需要替代。
  2. 相当于将Dijkstra Algorithm 进行升级 成从任意点到任意点的最小路径,当然前面算法的D数组和 P数组自然就变成二维数组 进行路径和权重的记录。
  3. 通过中间点找到更短值的就进行替换。最初的 D − 1 D^{-1} D1,经过一轮轮的跟新到 D 0 , D 1 , D 2 … … D^{0},D^{1},D^{2} …… D0,D1,D2

05 关于AOV AOE网

  1. AOV (Activity On Vertex Network) 分别表示活动网 有活动的先后顺序,对于边(弧)则代表着一种制约关系,具有顺序。
  2. AOE (Activity On Edge Network) 相当于弧长值 带有活动进行的时间的活动网。

5.1 拓扑排序

  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 );

    /* 找出 入度 为0 的顶点 压入堆栈 */
    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 ++;
        /* 对此顶点 所链接的后面 进行 遍历 删除此顶点的联系 使与之相连的 in 减1 */
        for( current =G->adjlist[gettop].firstedge; current; current =current->next ) {
            k =current->adjvex;
            if( !( -- (G->adjlist[k].in) ) ) { /* 如果被减到 0 则进行 入栈操作 */
                stack[++ top] =k;
            }
        }
    }

    if( count <G->numVertexes ) 
        return ERROR;
    else 
        return TRUE;
}
  1. 先转化到邻接表(顶点带有入度)的形式,进行循环 将所有入度为0 的顶点进行堆栈 PUSH操作,然后进行POP操作,将POP出的点 将与之所相连的顶点的入度-1,判断入度是否为0,是PUSH。(这就相当于 需要把本点前面的事情都做完 之后,才能来到这个节点 进行操作),相当于将 入度为0 的。

5.2 关键路径

  1. 应该是难理解的一个算法了
  2. 拓扑排序讲的是解决一个工程能否顺利进行的问题,而这里我们还需要关心 总的完成时间问题。理解为最关键的问题,每个把步骤最长的时间,就很紧密 中间无休息的那种,成为关键步骤。把具有最大长度的路径(时间),才是我们要找的关键路径。
  3. 理解几个参数etv(earliest time of vertex) 事件的最早发生时间 ==>ltv;
  4. 还有ete(earliest time of edge) 活动最早开始时间 ==> lte
  5. 求etv的代码:
#define ERROR 0
/* 全局变量定义区域 */
int *etv, *ltv; /* etv[]: (earliest time of vertex) */
int *stack2;    /* ltv[]: (latest time of vertex) */
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 );

    /* 找出 入度 为0 的顶点 压入堆栈 */
    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) ); /* etv[]数组申请内存 */
    for( i =0; i <G->numVertexes; i++ ) {
        etv[i] =0; /* etv[]数组 初始化 为0 */
    }
    stack2 =(int *)malloc( G->numVertexes *sizeof(int) ); /* 堆栈申请内存 */
    
    /* 开始主循环 */
    while( top !=0 ) {
        gettop =stack[top--];
        //printf("%d-> ", G->adjlist[gettop].data); /* 打印顶点 */       
        count ++;
        stack2[++top2] =gettop;
        /* 对此顶点 所链接的后面 进行 遍历 删除此顶点的联系 使与之相连的 in 减1 */
        for( current =G->adjlist[gettop].firstedge; current; current =current->next ) {
            k =current->adjvex;
            if( !( -- (G->adjlist[k].in) ) ) { /* 如果被减到 0 则进行 入栈操作 */
                stack[++ top] =k;
            }
            /* 求各顶点事件的最早发生值 即是对应的最大值 放到etv[] 数组里面 */
            if( etv[gettop] +current->weight >etv[k] ) {
                etv[k] =etv[gettop] +current->weight;
            }
        }
    }

    if( count <G->numVertexes ) 
        return ERROR;
    else 
        return TRUE;
}
  1. 求关键路径代码:
void critical_path( GraphList *G ) {
    EdgeNode *e;
    int i, j, k, gettop;
    int ete, lte; /* 分别定义最早发生时间 和最迟发生时间 */

    /* 拓扑序列 填满etv[] 数组 和 stack2 堆栈 */
    topo_logical_sort_v2( G );

    /* ltv[] 数组申请内存 和初始化 */
    ltv =(int *)malloc( G->numVertexes *sizeof(int) );
    for( i =0; i <G->numVertexes; i++ ) {
        ltv[i] =etv[G->numVertexes -1]; /* 以etv[] 数组最后一位 进行ltv[] 数组的初始化 */
    }

    /* 计算ltv[] */
    while( top2 !=0 ) { /* 其实恰好 将其顺序 颠倒过来 对每一个 gettop 所链接后面的表 进行计算 访问 */
        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;
            }
        }
    }

    /* 打印 关键 路径 ete =lte */
    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);
            }
        }
    }
}
  1. 更新etv[] 就是找出最长(最耗时的步骤)作为最早发生时间,就是从源点到该点所走路径达到的最大时间,就是从V0在走其它路径到这儿的话,用的时间都是 <= 最大时间。
  2. 同时在更新ltv[] 时候 ,对应于另一件事情 (<=最大时间的) 最短的时间 min(用最长时间 - 边的权重值(活动时间))
  3. 举个例子: etv[1] =3 ,ltv[1] =7 ,v1 这个时间最早也只能在第三天开始(按照总的工程进度来说话),同时 v1也可以在第七天开始 也可以不影响工期的完成。说明中间有四天的灵活时间,(像你是先写作业 还是先玩是一样的事情),如果灵活时间没有 ,则此时就是关键的路径了 时间紧凑的感觉。
  4. 活动的最早发生时间ete 就像当于 etv(事件的最早发生时间),活动的最迟发生时间lte[] 就是求最小的发生时间 相当于 最迟的发生时间(你这条路线再不去做的话,就是你耽误工程时间)(这一段 还需要细细理解 细细去品 !!!)

总结

  1. 第二遍去学习这个算法,然而第一遍当时挺明白,在第二遍的时候,又有重头再来的感觉,逻辑重新整理,又再次进入两天出来,所以这次要把学到的,看到的,记录下来 记录自己现在所看到的东西,于是用一天的时间来记录。
  2. 暂且把这当做过程吧! 慢啊慢啊 这应该就是学习。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值