数据结构第六章 图

系列链接
第一章 概述 第二章 线性表 第三章 栈与队列 第四章 串
第五章 树与二叉树 第六章 图 第七章 查找 第八章 排序

🔥基本概念

  • 图G记为G=(V,E)
    图

  • 顶点集V:所有顶点(结点)的集合,用|V|表示顶点个数

  • 边集E:所有边的集合,用|E|表示图G中边的条数

  • 图的顶点集不能是空的,但是边集可以是空的,边两头一定都有顶点

  • 无向图和有向图:E是无向边(边)的集合,(v,w)=(w,v),则G是无向图;E是有向边(弧)的集合,弧记为<v,w>,v是弧尾,w是弧头,<v,w>!=<w,v>,则G是有向图

  • 简单图和多重图:不存在重复边和顶点到自身的边的图是简单图,反之则为多重图。它们都分为有向和无向,我们主要讨论简单图。

  • 度:

  1. 无向图:顶点v的度是依附于该点的边数TD(v)
  2. 有向图:入度是以顶点v为终点(指向v)的有向边数ID(v),出度是以v为起点的有向边数OD(v),度是出度入度之和TD=ID+OD
  • 顶点之间关系:
  1. 路径:两点之间的线路的顶点序列,有向图要求考虑方向一致性
  2. 回路(环):第一个顶点即最后一个顶点的路径
  3. 简单路径:不重复经过结点的路径
  4. 简单回路:头尾相同的简单路径
  5. 路径长度:路径上的边数
  6. 点到点的距离;u到v的最短路径若存在即为距离,不存在则为无穷
  • 连通图:任意两点都连通(有路径)的无向图,则为连通图

  • 强连通图:任意两点都强连通(有路径)的有向图,则为强连通图

  • 子图:图的一部分(注意前提得是图)

  • 连通分量:无向图的极大连通子图称为连通分量,极大连通子图是必须连通且包含尽可能多的顶点和边的子图(尽可能多连)

  • 强连通分量:有向图的极大强连通子图称为强连通分量,极大强连通子图是必须强连通且包含尽可能多的顶点和边的子图

  • 生成树:连通图的生成树是包含图中全部顶点的一个极小连通子图(边尽可能少),不唯一

  • 生成森林:将图的连通分量分别生成生成树

  • 边的权:给边赋予的权值

  • 带权图:边带有权的图是带权图,也称为

  • 带权路径长度:带权图中,一条路径上所有边的权值之和

  • 其他图:

  1. 无向完全图:任何两点都存在边的无向图
  2. 有向完全图:任何两点都存在方向相反的两条弧的有向图
  3. 树:不存在回路且连通的无向图,即上一章的树,树属于图
  4. 森林:和树同理
  5. 有向树:一个顶点入度为0,其余顶点入度均为1的有向图

🔥图的存储结构

邻接矩阵法

邻接矩阵法

  • 矩阵的0和1代表两点之间的边存在与否
  • 无向图的邻接矩阵是对称矩阵,可以进行压缩存储
#define MaxVertexNum 100      //顶点数目最大值
typedef struct{
  char Vex[MaxVertexNum];     //顶点表
  int Edge[MaxVertexNum][MaxVertexNum];   //邻接矩阵,即边表,可以用char和bool减小空间
  int vexnum,arcnum;      //图的当前顶点树和边数/弧树
} MGraph;
  • 无向图的顶点的度等于该点所在行或列的非零元素个数;有向图顶点出度是非零元素个数,入度是非零元素个数,度是两者之和。
  • 对于带权图,只要把矩阵的0,1改成权值
  • 邻接矩阵空间复杂度很高,适合用于存放边很密的图(空间利用率大),
  • An[i][j]等于i到j顶点的长度为n的路径的数目,以此可以得到矩阵An,如下:
    邻接矩阵性质

邻接表法(顺序+链式)

邻接表法

  • 如上图,^代表空,求顶点度只需要查找对应的链表即可
  • 每个顶点链表的边顺序不唯一
  • 邻接表找入边很不方便,需要遍历整个邻接表。删除边也很不方便,需要删两份数据

十字链表法

十字链表存有向图

  • 如图,对于有向图,从一个顶点出发顺着橙色寻找可以找到所有指向它的弧,顺着绿色寻找可以找到所有指出的弧
  • 十字链表找出入边都方便,但只能存有向图

邻接多重表

邻接多重表存无向图

  • 类似十字链表,不会有冗余数据,删除方便
  • 邻接多重表只能存无向图

比较

存储方式

🔥图的基本操作

  • Adjacent(G,x,y):判断图G是否存在边<x, y>或(x, y)。
  • Neighbors(G,x):列出图G中与结点x邻接的边。
  • InsertVertex(G,x):在图G中插入顶点x。
  • DeleteVertex(G,x):从图G中删除顶点x。
  • AddEdge(G,x,y):若无向边(x,y)或有向边<x, y>不存在,则向图G中添加该边。
  • RemoveEdge(G,x,y):若无向边(x,y)或有向边<x, y>存在,则从图G中删除该边
  • FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。
  • NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。
  • Get_edge_value(G,x,y):获取图G中边(x, y)或<x, y>对应的权值。
  • Set_edge_value(G,x,y,v):设置图G中边(x, y)或<x, y>对应的权值为v

🔥图的遍历

广度优先遍历(BFS)

  • BFS的思路和树的广度优先遍历一样,借助辅助队列进行
bool visited[MAX_VERTEX_NUM];   //访问标记数组,已经访问过的点设为true
void BFS(Graph G,int v){    //从顶点v出发广度优先遍历G
  visit(v);       //访问操作
  visited[v]=true;    //标记已访问点
  Enqueue(Q,v);       //顶点v入队Q
  while(!isEmpty(Q)){   //一直遍历直到队列为空
    DeQueue(Q,v);     //顶点v出列
    for(w=FirstNeighbor(G,v);w>0;w=NextNeighbor(G,v,w))   //所有v的邻接点
      if(!visited[w]){    //访问未被访问过的邻接点
        visit(w);
        visited[w]=true;
        EnQueue[Q,w];
      }
  }
}
void BFSTraverse(Graph G){    //对G进行广度优先遍历
  for(i=0;i<G.vexnum;++i)
    visited[i]=false;     //访问标记数组的初始化
  InitQueue(Q);         //初始化辅助队列Q
  for(i=0;i<G.vexnum;++i)   //从0号顶点开始遍历
    if(!visited[i])     //对每个连通分量调用一次BFS
      BFS(G,i);       //对未被访问过的点进行BFS
}
  • BFSTraverse函数的存在保证非连通图也能遍历完全
  • 广度优先生成树:根据上述的BFS操作,我们可以根据遍历的先后获取一个树,这是因为一次遍历过程中,每个顶点是通过哪个点遍历到是唯一的,这恰恰是树的结构。邻接表遍历过程不唯一,生成树也就不唯一
    广度优先生成树
  • 广度优先生成森林:非连通图可以多个广度优先生成树,构成森林就是广度优先生成森林。
  • 有向图从不同点遍历,需要的次数可能不一样

深度优先遍历(DFS)

  • 一条路走到底再回头,类似树的先根遍历,利用递归实现
bool visited[MAX_VERTEX_NUM];     //访问标记数组
void DFS(Graph G,int v){
  visit(v);
  visited[v]=true;        //已访问标记
  for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
    if(!visited[w]){      //w为u尚未访问的邻接顶点
      DFS(G,w);
    }
}
void DFSTraverse(Graph G){
  for(v=0;v<G.vexnum;++v)
    visited[v]=false;
  for(v=0;v<G.vexnum;++v)   //从第一个顶点开始遍历
    if(!visited[v])     //遍历时访问过的就不用再遍历
      DFS(G,v);
}
  • 和BFS一样,使用DFSTraverse防止非连通图遍历不全的问题
  • 深度优先生成树:根据深度优先遍历获得的序列得到生成树,同广度优先生成树
  • 深度优先生成森林:类似广度优先生成森林

🔥最小生成树

  • 又名最小代价树生成树中边权值之和最小的即为最小生成树。研究对象是带权连通的无向图

Prim(普里姆)算法

  • 从某个顶点开始构建生成树,每次仅将代价(边的权值)最小的新顶点纳入生成树,直到所有顶点都纳入为止
  • 同一顶点Prim算法可以得到不同的生成树
  • 时间复杂度与顶点数相关,适用于边密的图

Kruskal(克鲁斯卡尔)算法

  • 在所有边中每次选择一条权值最小的边,使这条边两头连通(已连通的就不选),直到所有结点连通
  • 时间复杂度与边数相关,适合边稀疏的图

🔥最短路径

  • 分为单源最短路径(特定一个点分别到多点)和各顶点间最短路径(任何两点之间)
  • 单源最短路径方法:BFS算法(用于无权图),Dijkstra算法(带权、无权图)
  • 各顶点间最短路径:Floyd算法(带权、无权图)

BFS算法

  • 思路上就是修改BFS遍历算法,visit的具体操作是修改最短路径(d)和前驱结点(path)
void BFS_MIN_Distance(Graph G,int u){
  //d[i]表示u到i结点的最短路径
  for(i=0;i<G.vexnum;++i){
    d[i]=inf;       //初始化路径无穷大
    path[i]=-1;     //最短路径从哪个顶点来的(前驱结点)
  }
  d[u]=0;
  visited[u]=true;
  EnQueue(Q,u);
  while(!isEmpty(Q)){
    DeQueue(Q,u);
    for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
      if(!visited[w]){      //未访问过的邻接结点
        d[w]=d[u]+1;      //到这个点的路径等于到前驱结点路径+1
        path[w]=u;        //最短路径从u到w
        visited[w]=true;
        EnQueue(Q,w);     //w入队
      }
  }
}

BFS算法最短路径

  • 如上图,要知道2如何到8最短,只要顺着表格中path从8按前驱结点依次找回到2,其间经过的结点也是路径上的结点,8的d也就是最短路径长度,最短路径就是2->6->7->8
  • 对应广度优先生成树,我们可以看到每个结点所在的层数也能反映其最短路径:
    BFS最短路径与广度优先生成树

Dijkstra算法

Dijkstra算法思路

  • 如上图,以有向图为例,思路上是对于未确定最短路径的点不断更新其路径,需要n-1轮处理。思路和Prim算法的思路类似
    Dijkstra算法
  • 得到如上结果,就可以顺着表格找寻点1到任何一点的最短路径及其长度
  • Dijkstra算法不适用有负权值的边的图

Floyd算法

  • 利用动态规划思想,分阶段求解

    对于n个顶点的图G,求任意一对顶点Vi->Vj之间的最短路径可分为如下几个阶段:
    初始:不允许在其他顶点中转,最短路径是?
    0:若允许在Vo中转,最短路径是?
    1:若允许在Vo、V1中转,最短路径是?
    2:若允许在Vo、V1、V2中转,最短路径是?

    n-1:若允许在Vo、V1、V2. …..Vn-1中转,最短路径是?

Floyd算法

  • 如此得到2个矩阵,可以通过左边这个矩阵查找最短距离,右边这个矩阵查找中转点,如果不止一个中转点,需要考虑端点到中转点的最短路径,就是多查找表格几次,直到路径是存在的。
  • 代码上核心逻辑如下:
for(int k=0;k<n;k++){     //考虑Vk作为中转点
  for(int i=0;i<n;i++){   //遍历整个矩阵,i为行号,j为列号
    for(int j=0;j<n;j++){
      if(A[i][j]>A[i][k]+A[k][j]){  //考虑Vk作为中转点会不会缩短路径
        A[i][j]=A[i][k]+A[k][j];    //更新最短路径
        path[i][j]=k;         //中转点
      }
    }
  }
}
  • Floyd算法可以解决带负权值的图,但是不能解决带负权回路的图,如下图这样,每经过一圈,最短路径就会减小,相当于不存在最短路径。
    负权回路的图

🔥有向无环图的应用

描述表达式

  • 有向无环图(DAG):没有环路的有向图
  • 表达式可以用树来实现,对于有重复运算项的表达式,可以通过合并分支来生成DAG以减少结点数
  • 求解方法举例如下
    有向无环图解题举例

拓扑排序

AOV网

  • 拓扑排序是图论的概念,以上面这个AOV网为对象,简而言之是找到做事的先后顺序,这是不唯一的
  • 拓扑排序的实现:
  1. 从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
  2. 从网中删除该顶点和所有以它为起点的有向边。
  3. 重复1和2直到当前的AOV网为空或当前网中不存在无前驱的顶点为止
  • 逆拓扑排序:相当于和拓扑排序反着来,对一个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序(不唯一):
  1. 从AOV网中选择一个没有后继(出度为0)的顶点并输出。
  2. 从网中删除该顶点和所有以它为终点的有向边。
  3. 重复1和2直到当前的AOV网为空。
  • DFS算法中每进行一次DFS都打印v就可以得到逆拓扑排序序列

关键路径

  • AOE网:在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网(Activity On Edge NetWork)
    AOE网举例
  • AOE网具有以下两个性质:
  1. 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才
  2. 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。另外,有些活动是可以并行进行的
  • 在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;

  • 也仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。

  • 从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动

  • 完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长,关键路径问题其实就是小学学过的的时间安排问题

  • 关键路径的时间方面有以下几个参数:
    关键路径最早时间
    关键路径最迟时间

  • 求关键路径:

  1. 求所有事件的最早发生时间ve(),需要进行拓扑排序以方便逐个考虑事件
  2. 求所有事件的最迟发生时间vl(),需要进行逆拓扑排序以方便逐个考虑事件
  3. 求所有活动的最早发生时间e()
  4. 求所有活动的最迟发生时间I()
  5. 求所有活动的时间余量d(),d(i)=0即为关键活动,由关键活动即可获取关键路径
  • 当关键活动改变也会相应地改变关键路径,当关键活动缩短到一定程度可能改变关键路径,该活动可能会变成非关键活动,此时继续压缩该活动就不会缩短关键路径长度。所以显然,也可能存在多个关键路径。
  • Copyrights © 2023-2025 LegendLeo Chen
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信