06--图

一、认识图

  • 一些例子
    • 一个组织内部、不同部门员工之间的电子邮件网路
    • 互联网,全球WWW网页构成的图
    • 社会网络研究进展图
    • 一个跆拳道俱乐部内部成员的社交关系
    • facebook上宣称的好友关系和实质上长维护着的关系
    • 网页间相互连接的关系图
  • 图的基本术语
    • 图(又称网络):由节点和节点间的连边构成的对象称为“图”
    • 节点/结点(node),又称顶点(vetex)
    • 边(edge),又称链接(link),联系(tie),弧(arc)
    • 本介绍中网络指边带权的图,有向边
  • 图的ADT
    ADT Graph{
        数据对象:具有相同特性的数据元素的集合,称为顶点集$V$
        数据关系:$R=\{<v,w>|v,w\in V&&P(v,w)\}$
        基本操作:
          Create_Graph();
          GetVex(G,v);
          ...
          DFStraverse(G,v);
          BFStraverse(G,v);
    }
    
  • 图的类型
    • 无向图
    • 有向图
    • 完全图
  • 边相关的概念
    • 邻接点:用边链接的两个顶点互为邻接点,常指无向图
    • 边权值:描述边的属性或特定,若边无权可假设为固定常数
  • 顶点的度
    • 度:顶点链接的边的条数
    • 有向图的度为出度和入度二者之和
  • 路径相关
    • 在图G ( V , E ) (V,E) (V,E)中, 若从顶点 v i v_i vi出发, 沿E中的一些边经过一些 V V V中的顶点 v 1 , v 2 , ⋯   , v m v_1,v_2,\cdots,v_m v1,v2,,vm,到达顶点 v j v_j vj, 则称顶点序列 ( v i , v 1 , v 2 , ⋯   , v m , v j ) (v_i,v_1,v_2,\cdots,v_m,v_j) (vi,v1,v2,,vm,vj)为从顶点 v i v_i vi到顶点 v j v_j vj路径
    • 路径长度:非带权图为边的条数,带权图为权之和
    • 顶点互不重复的路径称为简单路径
    • 第一个顶点与最后一顶点重合的简单路径为简单回路
  • 子图相关
    • 在无向图中, 若从顶点 v 1 v_1 v1到顶点 v 2 v_2 v2有路径, 则称顶点 v 1 v_1 v1 v 2 v_2 v2是连通的。如果图中任意一对顶点都是连通的, 则称此图是连通图。非连通图的极大连通子图叫做连通分量
    • 在有向图中, 若对于每一对顶点 v i v_i vi v j v_j vj, 都存在一条从 v i v_i vi v j v_j vj和从 v j v_j vj v i v_i vi的路径, 则称此图是强连通图。非强连通图的极大强连通子图叫做强连通分量
  • 树相关的概念
    • 生成树:有 n n n个顶点和 e e e条边的连通图,其 n − 1 n − 1 n1条边和 n n n个顶点构成一个极小连通子图为此连通图的生成树
    • 一棵生成树,任意添加一条边,都会形成环
    • 删除生成树的任何一条边,则变成非连通图
    • 有向树:恰有一个顶点入度为0,其余顶点入度均为1的有向图
    • 生成森林:一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,且仅包含足以构成若干棵不相交的有向树的弧

二、图的存储结构

  • 邻接矩阵/数组表示法
    • 一个记录各个顶点信息的定点表+一个表示各顶点之间关系的邻接矩阵。更准确地说是“数组表示法”,一个一维顶点数组+一个二维边数组
    • 无向图
      • 邻接矩阵是对称的
      • 统计第 i i i行(列)1的个数可得顶点 i i i的度
    • 有向图
      • 邻接矩阵可能是不对称的
      • 统计第 i i i行1的个数可得顶点 i i i的出度,统计第 i i i列1的个数可得顶点 i i i的入度
    • 带权图:邻接矩阵上无边写 ∞ \infty ,有边写权值,对角线为0
    #define INFINITY INT_MAX //最大值
    #define MAX_VERTEX_NUM 20 //最大顶点个数
    typedef enum{DG,DN,UDG,UDN} GraphKind; //图的类型
    typedef struct ArcCell{  
      WType w; // WType为边权值类型
      InfoType *info; //该弧相关信息指针
    }ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
    
    typedef struct {
      VexType vexs[MAX_VERTEX_NUM]; //顶点向量
      AdjMatrix adj; //邻接矩阵
      int vexnum,arcnum; //顶点数和弧数
      GraphKind kind; //图的种类标志
    }MGraph;
    
  • 邻接表(广义表的链式存储结构)
    • 顶点结构:结构体数组,结构体包括数据域data和指向第一个依附该顶点的弧节点的指针firstarc
    • 弧节点结构:每个顶点依附的弧构成一个单链表;节点结构包括:弧的另一个顶点adjvex,指向下一个弧节点的指针nextarc和弧的信息info
    • 有向图
      • 头节点=顶点节点的数目,弧节点数目=弧节点数目
      • 邻接表:头节点代表的顶点是箭头的起点,弧节点中的顶点是箭头的指向
      • 邻接表求顶点的出度,就是求顶点后接的链表的长度
      • 逆邻接表求顶点的入度,就是求顶点后接的链表的长度
    • 无向图
      • 头节点=顶点节点的数目,弧节点数目=2倍边数目
      • 同一个顶点发出的边链接在同一个边链表中
      • 每条边被存在其两个顶点为头节点的边链表中
      • 邻接表求顶点度,就是求顶点后接的链表的长度
    • 在邻接表上易找出任一顶点的第一个邻接点和下一个邻接点
    #define MAX_VERTEX_NUM 20
    typedef struct ArcNode {
      int adjvex; // 该弧所指向的顶点的位置
      struct ArcNode *nextarc;// 指向下一条弧指针
      InfoType *info; // 该弧相关信息的指针
    } ArcNode;
    
    typedef struct VNode {
      VexType data; // 顶点信息
      ArcNode *firstarc; // 指向第一条依附该顶点的弧
    } VNode, AdjList[MAX_VERTEX_NUM];
    
    typedef struct {
      AdjList vertices;
      int vexnum,arcnum;//图的当前顶点数和弧数
      int kind;//图的种类标志
    }ALGraph;
    
    
  • 有向图的十字链表:融合邻接表和逆邻接表
    • data域:存储和顶点相关信息
    • 指针域firstin:指向以该节点为弧头的第一条弧所对应的弧节点
    • 指针域firstout:指向以该节点为弧尾的第一条弧所对应的弧节点
    • 尾域tailvex:表示弧尾顶点在图中的位置
    • 头域headvex:表示弧头顶点在图中的位置
    • 指针域hlink:指向弧头相同的下一条弧
    • 指针域tlink:指向弧尾相同的下一条弧
    • Info域:指向该弧的相关信息
    • 顶点节点(data,firstin,firstout)
    • 弧节点(tailvex,headvex,hlink,tlink,info)
    #define INFINITY MAX_VAL /*最大值∞*/
    #define MAX_VEX 30 //最大顶点数
    typedef struct ArcNode{
      int tailvex, headvex; //尾结点和头结点在图中的位置
      InfoType *info; //与弧相关的信息, 如权值
      struct ArcNode *hlink, *tlink;
    }ArcNode; //弧结点类型定义
    
    typedef struct VexNode{
      VexType data; // 顶点信息
      ArcNode *firstin, *firstout;
    }VexNode; //顶点结点类型定义
    
    typedef struct{
      int vexnum;
      VexNode xlist[MAX_VEX];
    }OLGraph; //图的类型定义
    
  • 无向图的邻接多重链表
    • 边节点(mark,ivex,ilink,jvex,jlink,info)
      • 标志域mark:用以表示该边是否被访问过
      • ivex和jvex域:分别保存该边所依附的两个顶点在图中的位置
      • info域:保存该边的相关信息
      • 指针域ilink:指向下一条依附于顶点ivex的边
      • 指针域jlink:指向下一条依附于顶点jvex的边
    • 顶点节点(data,firstedge)
      • data域:存储和顶点相关的信息
      • 指针域firstedge:指向依附于该顶点的第一条边所对应的表节点
    #define INFINITY MAX_VAL //最大值∞
    #define MAX_VEX 30 //最大顶点数
    typedef emnu {unvisited, visited} Visitting;
    typedef struct EdgeNode{
      Visitting mark; //访问标记
      int ivex, jvex; //该边依附的两个结点在图中的位置
      InfoType *info; //与边相关的信息, 如权值
      struct EdgeNode *ilink, *jlink;
      // 分别指向依附于这两个顶点的下一条边
    }EdgeNode; //弧边结点类型定义
    
    typedef struct VexNode{
      VexType data; //顶点信息
      ArcNode *firstedge; //指向依附于该顶点的第一条边
    }VexNode; //顶点结点类型定义
    
    typedef struct{
      int vexnum;
      VexNode mullist[MAX_VEX];
    }AMGraph;
    

三、图上的搜索

  • 输入:图 G = ( V , E ) G=(V,E) G=(V,E),初始顶点 v 0 v_0 v0,目标测试函数 G o a l ( s ) Goal(s) Goal(s)判断顶点 s s s是否满足给定条件,满足给定条件返回 t r u e true true否则返回 f a l s e false false
  • 输出:从给定初始顶点 v 0 v_0 v0出发沿着边到达目标顶点的路径
    • 一条路径或多条路径:最优(最短)路径
    • 退化时,不需要路径,只要目标状态
  • 典型的不同目标测试函数 G o a l ( ) Goal() Goal()
    • 找到指定编号的顶点(返回路径,应用案例:迷宫寻路)
    • 找到一个或所有红色(指某个特定属性)的顶点(返回目标顶点或路径,应用案例:n皇后或华容道)
    • 输出连通图每个顶点的着色信息恰好一次(应用案例:遍历图)
  • 图的搜索算法框架
    • 搜索过程中图 G G G的节点分为三类:访问过的节点,访问过节点的直接邻接点(搜索的边界),其它未被访问过的节点
    Created with Raphaël 2.3.0 开始 初始化路径搜索边界 起点是目标节点? 返回路径 结束 起点存入搜索边界 时间未耗尽 搜素边界为空集 返回失败,无解 结束 搜索边界中选择一个节点删除,保存为s 若s未被访问过 访问s 更新路径 检验将s的所有邻接点是否是目标节点,若不是,则存入搜索边界 结束 yes no yes no yes no yes no
    Input:图G;初始节点s0;
    Output: path:代表解的路径
    path<--(s0),FRINGE<--φ  //初始化,其中FRINGE是保存搜索边界的数组
    if(GOAL(s0)==T)
      return path=(s0);
    INSERT(s0,FRINGE);
    while T do
      if(isEmpty(FRINGE==T))
        return failure; //表示无解
      s<--REMOVE(FRINGE)
      if(s未访问过)
      {
        visit(s);
        update path;
        foreach s的邻接点s'' do
        {
            if(GOAL(s'')==T)
              return (path, s'');
            INSERT(s'',FRINGE);
        }
        end
      }
      end
    end
    
  • 图的搜索的各种不同变型
    • 各种搜索算法,不同之处在于对搜索边界中节点的次序确定方法
    • 更具体地说:就是 I N S E R T ( s ′ , F R I N G E ) INSERT(s',FRINGE) INSERT(s,FRINGE)函数地实现方法不同
    • 广度优先搜索:每次插入 s ′ s' s时,放在 F R I N G E FRINGE FRINGE地末尾
    • 深度优先搜索:每次插入 s ′ s' s时,放在 F R I N G E FRINGE FRINGE地开头
    • 启发式搜索算法:依据问题地特定,设计一个节点排序地策略或方法,将 s ′ s' s插入在 F R I N G E FRINGE FRINGE地某个特殊位置
      • 贪婪/贪心算法:只考虑从当前节点到目标节点地最短路径
      • A*算法:考虑从出发点开始,到目标点地最短路径
    • 搜素策略:本质上就是决定哪一个节点放在 F R I N G E FRINGE FRINGE开头,不排序,只选择最有“希望地”节点
  • 图搜索中的技术问题
    • 如果一个节点被多次访问会出现什么问题
      • 算法不会停止,构成三角循环
    • 防止节点被多次访问
      • 节点数目少时:用一个标志数组 b o o l v i s i t e d [ ] bool visited[] boolvisited[]来记录图的每个节点是否被访问过
      • 节点数目多时:内存中仅保存已经访问过的节点和FRINGE中的节点
    • 如何保存和更新路径
      • 设计数据结构保存信息:经过的边数,经过边上权值之和,节点是否已扩展过
      • 输出一棵树时,每个子节点指向其双亲结点(扩展该子节点的节点);根的出度为0,树的双亲表示法
      • 输出路径时,从找到的目标节点,沿双亲结点上行,直至根节点
  • 图搜索的时空复杂性
    • 普通图(非指数图):图中所有顶点和边都可以在内存中同时保存
      • 搜索目标是必须返回路径,空间需求 O ( n ) O(n) O(n),时间 O ( n + e ) O(n+e) O(n+e)
      • 搜索目标仅需返回与路径无关的目标节点:平均查找时间为 ( n + 1 2 ) (\frac{n+1}{2}) (2n+1),空间需求 O ( 1 ) O(1) O(1)
    • 指数图(图中顶点最大度为b,m为搜索树叶子的最大深度,d为搜索树种埋藏最浅的目标节点深度)
      • 广度,均为 O ( b d ) O(b^d) O(bd)
      • 深度,时间 O ( b m ) O(b^m) O(bm),空间 O ( b m ) O(bm) O(bm)
      • 回溯算法,深度优先的改进,搜索树的每一层只保留一个邻居节点到 F R I N G E FRINGE FRINGE,空间开销是 O ( m ) O(m) O(m)
  • 在连通图和非连通图上的图搜索算法
    • 连通图
      • 初始节点和目标节点位于一个连通片,有解
      • 图为便无权或等权时,广度优先搜索一定能找到最短路径,深度优先搜索不一定能找到最短路径
    • 非连通图
      • 可能不存在从初始节点到目标节点的路径,无解

四、图的遍历:仅适用于非指数图

  • 图的遍历:从图中某一顶点出发,沿着图的边,访遍图中所有的顶点,且使每个顶点仅被访问一次,这个过程叫做图的遍历
  • 深度优先搜索遍历
    • 一直找,找不到回退,回退后能找一直找,找不到回退,直至结束
    • 采用邻接矩阵实现算法 O ( n 2 ) O(n^2) O(n2)
    • 采用邻接表实现算法 O ( n + e ) O(n+e) O(n+e)
    bool visited[MAX];
    Status (* VisitFunc)(int v);
    void DFSTraverse (Graph G,Status (* Visit)(int v))
    {
      VisitFunc =Visit;
      for ( v= 0; v < G.vexnum; v++ )
        visited [v] = FALSE; //访问数组 visited 初始化
      for ( int v = 0; v < G.vexnum; v++ )
        (!visited[v]) DFS(G, v);//遍历每个连通片
    }
    
    void DFS (Graph G, int v) 
    { //递归函数
      visited[v] = TRUE; //顶点 v 作访问标记
      VisitFunc(v); //访问顶点 v
      for (w=FirstAdjVex(G,v); w>=0; w=NextAdjVex(G,v,w))
        if ( !visited[w] ) DFS (G, w);
    //若顶点 w 未访问过, 递归访问顶点 w
    }
    
  • 广度优先搜索遍历
    • 依次访问起始顶点的每个邻接顶点,直至所有顶点被访问
    • 时间复杂度同深度优先搜索遍历
    void BFSTraverse(Graph G,Status (* Visit)(int v))
    {
      for (v=0;v<=G.vexnum;++v)
        visited[v]=FALSE; //初始化数组
      InitQueue(Q); //初始化队列
      for (v=0;v<=G.vexnum;++v)
        if (!visited[v])
        {
          visited[v]=TRUE;
          Visit(v); //标记顶点并访问
          EnQueue(Q,v); //入队列
          while (!QueueEmpty(Q))
          {
            DeQueue(Q,u); //出队列
            //遍访u的所有邻接点
            for (w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w))
            if (!Visited[w]) 
            {
              Visited[w]=TRUE; Visit(w);
              EnQueue(Q,W); //入队列
            }
          }
        }
    }
    
  • 图遍历的输出
    • 生成树和生成森林
      • 图的搜索,输出是目标节点或到达目标节点的路径
      • 图的遍历,我们定义其输出为图搜索时输出访问的节点以及访问该节点的原因:连通图(生成树),非连通图(生成森林)
    • 深度优先搜索
      • 不能确定输出的生成树有多少个孩子,因此采用孩子-兄弟链表来存储生成树
      • 算法思想:递归
        • 从某个顶点 v v v出发,建立一个树节点 T T T
        • v v v的一个邻接点为起始点,建立子生成树
        • 将子生成树作为 T T T的节点的子树,连接为 T T T节点的左孩子
        • 第2,3…个未被访问邻接点为起始点,建立子生成树,并连接为 T T T或前一棵子生成树的右孩子
    typedef struct CSNode
    {
      ElemType data;
      struct CSNode *firstchild, *nextsibling;
    }CSNode;
    
    CSNode* DFSTree(ALGraph *G, int v)
    {
      CSNode* T,*ptr,*q;
      ArcNode *p;
      int w;
      visisted(v);
      Visited[v]=TRUE;
      T=(CSNode*)malloc(sizeof(CSNode));
      T->data=G->vertices[v].data;
      T->firstchild=T->nextsibling=NULL;
      q=NULL;
      p=G->vertices[v].firstarc;
      while(p!=NULL)
      {
        w=p->adjvex;
        if(!Visited[G,w])
        {
          ptr=DFSTree(G,w);
          if(q==NULL)
            T->firstchild=ptr;
          else
            q->nextsibling=ptr;
          q=ptr;
        }
      p=p->nextarc;
      }
      return T;
    }
    
    • 广度优先搜索
      • 不能确定输出的生成树有多少个孩子,因此采用孩子-兄弟链表来存储生成树
    typedef struct Queue
    {
      int elem[MAX_VEX]
      int front,rear;
    }Queue;
    
    CSNode* BFSTree(ALGraph* G, int v)
    {
      CSNode* T, *ptr, *q, *now;
      CSNode* vertices[MAX_VERTEX_NUM]={NULL};
      ArcNode *p;
      Queue Q;
      int w,k;
      for(int i=0; i<G->vexnum;i++)
      {
        vertices[i]=(CSNode*)malloc(sizeof(CSNode));
        assert(vertices[i]);
        vertices[i]->data=G->vertices[i].data;
        vertices[i]->firstchild=NULL;
        vertices[i]->nextsibling=NULL;
      }
      bool visited[MAX_VERTEX_NUM]={false};
      Q->front=Q->rear=0;
      Visited[v]=TRUE;
      T=(CSNode*)malloc(sizeof(CSNode));
      assert(T);
      T->data=G->vertices[v].data;
      T->firstchild=T->nextsibling=NULL;
    
      Q->elem[Q.rear++]=v;
      while(Q.front != Q.rear)
      {
        w=Q.elem[Q.front++];
        q=NULL;
        p=G->vertives[w].firstarc;
        now=vertives[p.adjvex];
        while(p!=NULL)
        {
          k=p->adjvex;
          if(!visited[k])
          {
            Visited[k]=TRUE;
            ptr=vertices[k];
            if(q==NULL)
              now->firstchild=ptr;
            else
              q->nextsibling=ptr;
            q=ptr;
            Q->elem[Q.rear++]=k;
          }
          p=p->nextarc;
        }
      }
      return T;
    }
    
  • 将图的遍历应用于图的连通性
    • 求无向图的各个连通分量
      • 从任意结点开始,视为初始节点,执行图搜索算法,直至算法结束,得到 V 1 V_1 V1;选择任意未访问过的节点未初始节点,再次执行图搜索算法,得到 V 2 V_2 V2,…,不断重复,直到所有顶点都被访问过
    • 有向图的强连通分量
      • G G G进行深度优先遍历,生成 G G G的深度优先生成森林 T T T
      • 对森林 T T T的顶底按后根遍历顺序进行编号
      • 改变 G G G中每一条弧的方向,构造新的有向图 G ′ G' G
      • 按所标的顶点编号,从编号最大的顶点开始对 G ′ G' G进行深度优先搜索,得到一棵深度优先生成树,从未访问的顶点中选择编号最大的顶点,再进行深度优先搜索,直至所有顶点被访问

五、生成树与最小生成树

  • 生成树:使用不同的遍历方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树
  • 最小生成树:构造准则
    • 必须使用且仅使用该网络中的 n − 1 n-1 n1条边来连接网络中的 n n n个顶点
    • 不能使用产生回路的边
    • 各边上的权值总和达到最小
  • 普里姆(Prim)算法
    • 具体算法:见图论-02
    • 邻接矩阵作为图/网络的存储表示
    typedef struct{ //时间复杂度为 O(n^2), n 是节点数目, 它适用于稠密网
      VexType adjvex;
      WType lowcost;  //0:已经加入U;无穷大:无直接连边
    } closedge[MAX_VERTEX_NUM];
    
    void MiniSpanTree_PRIM(MGraph G, VexType u)
    {
      f=LocateVex(G,u);
      for (j=0;j<G.vexnum;++j)
        if (j!=f) 
          closedge[j]={u,G.adj[f][j].w};
      closedge[f].lowcost =0; //初始,U={u}
      for (i=0;i<G.vexnum&&i!=f;++i)
      {
        k=minimum(closedge); //非0最小权值的下标
        printf(closedge[k].adjvex,G.vexs[k]);
        closedge[k].lowcost = 0; //k并入U集
        for (j=0;j<G.vexnum;++j)//调整辅助数组
          if(G.adj[k][j].w<closedge[j].lowcost)
            closedge[j]={G.vexs[k],G.adj[k][j].w};
      }//各边有相同权值时,因选择的随意性,生成树可能不唯一
    }//各边的权值不相同时,产生的生成树是唯一的
    
    • 克鲁斯卡尔(Kruskal)算法
      • 具体算法:见图论-02
      • 时间复杂度 O ( e l o g e ) O(eloge) O(eloge)

六、AOV网络

  • 用顶点表示活动的网络(AOV网络)
  • 用有向图表示一个工程。在这种有向图中,用顶点表示活动,用有向边 < v i , v j > <v_i,v_j> <vi,vj>表示活动 v i v_i vi必须先于活动 v j v_j vj进行。这种有向图叫做顶点表示活动的AOV网络
  • 在AOV网络中不能出现有向回路,即有向环,否则某活动以自己为先决条件
  • 检测AOV网络中是否有环
    • 拓扑排序:即将各个顶点(活动)排列成一个线性有序的序列,使得AOV网络中所有应存在的前驱和后继关系都能得到满足
    • 算法思想
      • 输入AOV网络,令 n n n为顶点个数
      • 在AOV网络中选一个没有直接前驱的顶点,并输出之
      • 从图中删去该顶点,同时删去所有它发出的有向边
      • 重复2、3步,直到全部顶点均已输出
    void count_indegree(ALGraph *G)
    {
      int k; ArcNode *p;
      for (k=0;k<G->vexnum;k++)
        G->vertices[k].indegree=0; //顶点入度初始化
      for(k=0;k<G->vexnum;k++)
      {
        p=G->vertices[k].firstarc;
        while (p!=NULL)
        { //顶点入度统计
          G->vertices[p->adjvex].indegree++;
          p=p->nextarc;
        }
      }
    }
    
    int Topologic_Sort(ALGraph *G, int topol[])
    {
    //顶点的拓扑序列保存在一维数组topol中
      int k, no, vex_no, top=0, count=0, boolean=1;
      int stack[MAX_VEX]; //用作堆栈
      ArcNode *p;
      count_indegree(G); //统计各顶点的入度
      for (k=0;k<G->vexnum;k++)
        if (G->vertices[k].indegree==0)
          stack[top++]=k;
      do
      {
        if (top==0)  
          boolean=0;
        else
        {
          no=stack[top--]; //栈顶元素出栈
          topol[count++]=no; //记录顶点序列
          p=G->vertices[no].firstarc;
          while (p!=NULL)
          { //删除以顶点为尾的弧
            vex_no=p->adjvex;
            G->vertices[vex_no].indegree--;
            if(G->vertices[vex_no].indegree==0)
              stack[top++]=vex_no;
            p=p->nextarc;
          } // end while
        } //end if
      }while(boolean==1);
      if (count<G->vexnum) 
        return(-1);
      else 
        return(1);  
    }
    
    • 时间复杂度 O ( n + e ) O(n+e) O(n+e)

七、AOE网络

  • 与AOV网络相对应的AOE,是边表示活动的有向无环图
  • 图中定点表示事件,每个事件表示在其前的所有活动已经完成,其后的活动可以开始,弧表示活动,弧上的权值表示相应活动所需的事件或费用
  • 工程完成最短时间:从起点到终点的最长路径长度
  • 长度最长的路径称为关键路径,关键路径上的活动称为关键活动。关键活动是影响整个工程的关键
  • AOE网络的术语与符号
    • 若活动 a i a_i ai是弧 < j , k > <j,k> <j,k>,持续时间是 d u t ( < j , k > ) dut(<j,k>) dut(<j,k>)
    • e ( i ) e(i) e(i):表示活动 a i a_i ai的最早开始时间
    • l ( i ) l(i) l(i):在不影响进度的前提下,表示活动 a i a_i ai的最晚开始时间;则 l ( i ) − e ( i ) l(i)-e(i) l(i)e(i)表示活动 a i a_i ai的时间余量,若 l ( i ) − e ( i ) = 0 l(i)-e(i)=0 l(i)e(i)=0,表示活动 a i a_i ai是关键活动
    • v e ( i ) ve(i) ve(i):表示事件 v i v_i vi的最早发生时间,即从起点到顶点 v i v_i vi的最长路径长度
    • v l ( i ) vl(i) vl(i):表示事件 v i v_i vi的最晚发生时间。则有关系
      • e ( i ) = v e ( j ) e(i)=ve(j) e(i)=ve(j)
      • l ( i ) = v l ( k ) − d u t ( < j , k > ) l(i)=vl(k)-dut(<j,k>) l(i)=vl(k)dut(<j,k>)
    • 理解 v e ( i ) ve(i) ve(i)
      • v e ( j ) = { 0 j = 0 , 表 示 v j 是 起 点 M a x { v e ( i ) + d u t ( < i , j > ) ∣ < v i , v j > 是 网 中 的 弧 } 其 它 ve(j)=\begin{cases}0 & j=0,表示v_j是起点 \\ Max\{ve(i)+dut(<i,j>)|<v_i,v_j>是网中的弧\} & 其它\end{cases} ve(j)={0Max{ve(i)+dut(<i,j>)<vi,vj>}j=0,vj
      • 只有 v j v_j vj的所有前驱事件 v i v_i vi v e ( i ) ve(i) ve(i)算出后,才能计算 v e ( j ) ve(j) ve(j)
      • 方法:对所有事件进行拓扑排序,再按拓扑排序顺序计算每个事件的最早发生事件
    • 理解 v l ( j ) vl(j) vl(j)
      • v l ( j ) = { v e ( n − 1 ) j = n − 1 , 表 示 v j 是 终 点 M i n { v l ( k ) − d u t ( < j , k > ) ∣ < v j , v k > 是 网 中 的 弧 } 其 它 vl(j)=\begin{cases}ve(n-1) & j=n-1,表示v_j是终点 \\ Min\{vl(k)-dut(<j,k>)|<v_j,v_k>是网中的弧\} & 其它\end{cases} vl(j)={ve(n1)Min{vl(k)dut(<j,k>)<vj,vk>}j=n1,vj
      • v j v_j vj的所有后继事件 v k v_k vk的最晚发生时间 v l ( k ) vl(k) vl(k)计算出来后,才能计算 v l ( j ) vl(j) vl(j)
      • 按拓扑排序的逆顺序,依次计算每个事件的最晚发生时间
  • 算法思想:见图论-08
    void critical_path(ALGraph *G)
    {
      int j,k,m;
      ArcNode *p;
      int toppol[MAX_VER],ve[MAX_VER],vl[MAX_VER];
      if(Topologic_Sort(G,topol)==-1)
        printf("\nAOE网络存在回路,错误!\n");
      else
      {
        for(j=0;j<G->vexnum;j++)
          ve[j]=0;  //时间最早发生时间初始化
        for(m=0;m<G->vexnum;m++)
        {
          j=topol[m]; //存放了拓扑有序序列
          p=G->vertices[j].firstarc;
          for(;p!=NULL;p=p->nextarc)
          {
            k=p->adjvex;
            if(ve[j]+p->weight>ve[k])
              ve[k]=ve[j]+p->weight;
          }//遍历拓扑序列中第m各顶点的边链表
          //更新被指向节点的ve(k),求最大值
        }//从拓扑有序序列的第一个开始,依次计算每个事件最早发生时间
        for(j=0;j<G->vexnunm;j++)
        {
          vl[j]=ve[j];  //事件最晚发生时间初始化
          for(m=G->vexnum-1;m>=0;m--)
          {
            j=topol[m];
            p=G->vertices[j].firstarc;
            for(;p!=NULL;p=p->nextarc)
            {
              k=p->adjvex;
              if(vl[k]-p->weight<vl[j])
                vl[j]=vl[k]-p->weight;
            }//遍历拓扑序列中第m各顶点的边链表更新被指向节点的vl(k)
          }//从拓扑有序序列的最后一个开始,依次计算每个事件的vl值
        }
        for(m=0;m<G->vexnum;m++)
        {
          p=G->vertices[m].firstarc;
          for(;p!=NULL;p=p->nextarc)
          {
            k=p->adjvex;
            if((ve[m]+p->weight)==vl[k])
              printf("<%d, %d>", m, k);
          }//检查每个顶点的每条边,是否是关键活动
        }//输出所有关键活动
      } //end of else
    }
    
    • 时间复杂度 O ( n + e ) O(n+e) O(n+e)

最短路径

  • 从图中某一顶点到达另一顶点的路径可能不止一条,找到一条路径使得沿此路径上各边上的权值总和达到最小
  • 边上权值非负情形的单源最短路径问题——Dijkstra算法
  • 边上权值为任意值的单源最短路径问题——Bellman和Ford算法
  • 所有顶点之间的路径——Floyd算法
  • Dijkstra算法
    • 具体算法:见图论-01
    • 引入辅助变量D:存储已找到的各个最短路径长度
      • 它的每一个分量 D [ i ] D[i] D[i]表示当前找到的源点 v v v到终点 v i v_i vi的最短路径长度,初始为边权或 ∞ \infty
      • 每次求得一条最短路径后,其重点 v j v_j vj加入集合 S S S,然后对所有的 v k ∈ V − S v_k\in V-S vkVS,修改其 D [ i ] D[i] D[i]的值
    • 算法流程
      • 初始化
        • S ← { v } S\leftarrow \{v\} S{v}
        • D [ i ] ← a d j [ L o c a t e V e x ( G , v ) ] [ i ] D[i]\leftarrow adj[LocateVex(G,v)][i] D[i]adj[LocateVex(G,v)][i]
      • 选择 v j v_j vj使得
        • D [ j ] ← M i n { D [ i ] } , v i ∈ V − S D[j]\leftarrow Min\{D[i]\},v_i\in V-S D[j]Min{D[i]},viVS
        • S ← S ∪ { j } S\leftarrow S\cup\{j\} SS{j}
      • 修改
        • D [ k ] ← M i n { D [ k ] , D [ j ] + a d j [ j ] [ k ] , v k ∈ V − S } D[k]\leftarrow Min\{D[k],D[j]+adj[j][k],v_k\in V-S\} D[k]Min{D[k],D[j]+adj[j][k],vkVS}
      • 判断
        • S = V S=V S=V,则算法结束,否则转步骤2
    • 时间复杂度 O ( n 2 ) O(n^2) O(n2)
  • Floyd算法
    • 算法思想:设顶点集 S S S(初值为空),用数组 A A A的每个元素 A [ i ] [ j ] A[i][j] A[i][j]保存从 v i v_i vi只经过 S S S中的顶点到达 v j v_j vj的最短路径长度
      • 初始时令 S = { } S=\{\} S={} A [ i ] [ j ] = { 0 i = j W i j i ≠ j , < v i , v j > ∈ E , w i j : w e i g h t ∞ i ≠ j , < v i , v j > ∉ E A[i][j]=\begin{cases}0 & i=j \\ W_{ij} & i\neq j,<v_i,v_j>\in E,w_{ij}:weight \\ \infty & i\neq j, <v_i,v_j>\notin E\end{cases} A[i][j]=0Wiji=ji=j,<vi,vj>E,wij:weighti=j,<vi,vj>/E
      • 将图中一个顶点 v k v_k vk加入到 S S S中,修改 A [ i ] [ j ] A[i][j] A[i][j]的值
        • A [ i ] [ j ] = M i n { A [ i ] [ j ] , A [ i ] [ k ] + A [ k ] [ j ] } A[i][j]=Min\{A[i][j],A[i][k]+A[k][j]\} A[i][j]=Min{A[i][j],A[i][k]+A[k][j]}
      • 重复前一步骤,直到所有顶点加入到 S S S
    int A[MAX_VEX][MAX_VEX];
    int Path[MAX_VEX][MAX_VEX];
    void Floyd_path (MGraph *G)
    {
      int j, k, m;
      for(j=0;j<G->vexnum;j++)
        for(k=0;k<G->vexnum;k++)
        {
          A[j][k]=G->adj[j][k];
          Path[j][k]=-1;
        }//各数组的初始化
        for(m=0;m<G->vexnum;m++)
          for(j=0;j<G->vexnum;j++)
            for(k=0;k<G->vexnum;k++)
              if ((A[j][m]+A[m][k])<A[j][k])
              {
                A[j][k]=A[j][m]+A[m][k];
                Path[j][k]=m;
              }//修改数组A和Path的元素值
              for(j=0;j<G->vexnum;j++)
        for(k=0;k<G->vexnum;k++)
        if (j!=k)
        {
          printf("%d到%d的最短路径为:\n", j, k);
          printf("%d",j) ; prn_pass(j, k);
          printf("%d", k);
          printf("最短路径长度为:%d\n",A[j][k]);
        }
    } // end of Floyd
    void prn_pass(int j,int k)
    {
      if (Path[j][k]!=-1)
      {
        prn_pass(j,Path[j][k]);
        printf(",%d",Path[j][k]);
        prn_pass(Path[j][k],k);
      }
    }
    
    • 时间复杂度 O ( n 3 ) O(n^3) O(n3),空间复杂度 O ( n 2 ) O(n^2) O(n2)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值