数据结构-王道-图

目录

图的基本概念

       图\(G\)由顶点集\(V\)和边集\(E\)组成,记为\(G=(V,E)\),其中\(V(G)\)表示图\(G\)中顶点的有限非空集;\(E(G)\)表示图\(G\)中顶点之间的关系(边)集合。若\(V={v_1,v_2,v_3,\ldots,v_n}\),用\(|V|\)表示图\(G\)中顶点的个数,也称为图\(G\)的阶,\(E={(u,v)|u\in V,v\in V}\),用\(E\)表示图\(G\)中边的条数。

注意:线性表可以是空表,树可以是空树,但图不可以是空图。也就是说,图中不能一个顶点也没有,图中顶点集\(V\)一定非空,但是边集\(E\)可以为空,此时图中只有顶点而没有边。

有向图

       若E是有向边(也称为弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为\(<v,w>\),其中v,w是顶点。w称为弧头,v称为弧尾,称为从顶点v到顶点w的弧,也称为v邻接到w,或w邻接自v。

《数据结构-王道-图》

       上图
\((a)\)所示的有向图
\(G_1\)可表示为:

\(G_1=(V_1,E_1)\)
\(V_1={1,2,3}\)
\(E_1={<1,2>,<2,1>,<2,3>}\)

无向图

       若\(E\)是无向边(简称边)的有限集合时,则图\(G\)为无向图。边是顶点的无序对,记为\((v,w)\)\((w,v)\),以为\((v,w)=(w,v)\),其中\(v\)\(w\)是顶点。可以说顶点\(w\)和顶点\(v\)互为邻接点。边\(<v,w>\)依附于顶点\(w\)\(v\),或者说边\((v,w)\)和顶点\(v,w\)相关联。
       上图\((b)\)所示的无向图\(G_2\)可表示为:

\(G_2=(V_2,E_2)\)
\(V_2={1,2,3,4}\)
\(G_2={(1,2),(1,3),(1,4),(2,3),(2,4)(3,4)}\)

简单图

       一个图G如果满足:

  1. 不存在重复边。
  2. 不存在顶点到自身的边。
    则可以称为简单图。上图\((a),(b)\),都是简单图,并且数据结构中只讨论简单图。

    多重图

           若图\(G\)中,某两点之间的边数多于一条,又允许顶点通过一条边和自己关联,则G为多重图。多重图的定义和简单图是相对的。

    完全图

           在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有\(\frac{n(n-1)}{2}\)条边。在有向图中,如果任意两个顶点之间都存在方向相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有\(n(n-1)\)条有向边。
           上图中\((b)\)为无向完全图,\((c)\)为有向完全图。

    子图

           设有两个图\(G=(V,E)\)\(G^`=(V^`,E^`)\),若\(E^`\)\(E\)的子集,\(V^`\)\(V\)的子集,则称\(G`\)\(G\)的子图。
           上图中\((c)\)\((a)\)的子图。
    注意:并非\(V\)\(E\)的任何子集都能构成\(G\)的子图,因为这样的子集可能不是图,也就是说,\(E\)的子集中的某些边关联的顶点可能不再这个\(V\)的子集中。

    连通,连通图和连通分量

           在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连通的,则称G为连通图,否则称为非连通图。无向图中极大连通子图称为连通分量。如果一个图中有n个顶点,并且有小于n-1条边,则此图必是非连通图,如下图所示有三个连通分量(极大连通子图)。

    《数据结构-王道-图》

    注意:弄清连通(若顶点v到顶点w是有路径的则称:v和w是连通的),连通图(任意两个顶点之间都是连通的,则称该图为连通图)。连通分量(又称为:极大连通子图。极大连通子图是无向图的连通分量,极大要求该连同子图包含其所有的边;极小连通子图即要保持图连通,又要使边数最少的子图。)

    强连通图,强连通分量

           在有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连同的。若图中任何一对顶点都是强连同的,则称此图为强连通图。有向图中的极大连通子图称为有向图的强连通分量。

    生成树,生成森林

           连通图的生成树是包含图中全部顶点的一个极小连通图。如图中顶点数为n,则他生成的树含有n-1条边。对于生成树而言,若砍去她的一条边,则会编程非连通图,若加上一条边则会编程一个回路。在非连通图中,连同分量的生成树构成了非连通图的生成森林。 上图\((G_2)\)的一个生成树如图所示.

    《数据结构-王道-图》

    顶点的度,入度和出度

           图中每个顶点的度定义为以该顶点为一个端点的边得数目。
           对于无向图,顶点v的度是指依附于该顶点的边的条数,记为\(TD(v)\)
           在具有n个顶点e条边的无向图中,有\(\sum_{i=1}^nTD(V_i)=2e\)。即无向图的全部顶点的度之和等于边数的两倍,这是因为每条边和两个顶点相关联。具体可以参考上一个图。
           对于有向图,顶点v的度分为入度和出度,入度是以顶点v为终点的的有向边的数目,记为\(ID(v)\);而出度是以v为起点的有向边的数目,记为\(OD(v)\)顶点v的度等于其入度和出度之和
           在具有n个顶点和e条边的有向图中,有每个点的入度之和=每个点的出度之和=e,这是因为每条边都有一个起点和终点。

    边的权和网

           在一个图中,每条边可以表上具有某种意义的数值,该数值称为改边的权值。这种边上带有权值的图称为带全图,也称作网。

    稠密图,稀疏图

           边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图本身是模糊的概念,稀疏图和稠密图一般是相对而言的。一般图G满足\(|E|<|V|*\log|V|\)时,可以将G看做是稀疏图。

    路径,路径长度和回路

           顶点\(v_p\)到顶点\(v_q\)之间的一条路径是指顶点序列\(v_p,v_{i1},v_{i2},\ldots,v_{im},v_q\)。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或者环。如果一个图有n个顶点,并且有大于n-1条边,则此图一定有环。

    简单路径,简单回路

           在路径序列中,顶点不重复重现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路称为简单回路。

    距离

           从顶点u出发到顶点v的最短路径若存在,则此路径的长度称作从u到v的距离。若从u到v不存在路径,则记距离为\(\infty\)

    有向树

           有一个顶点的入度为0,其余顶点的入度均为1的有向图称为有向树。

    图的存储和基本操作

           图的存储必须要完整,准确的反应顶点集和边集的信息。根据不同图的结构和算法,可以采用不同的存储方式,但不同的存储方式对程序的效率影响相当的大。因此所选的数据结构应该适合于待求解的问题。不论是有向图还是无向图,主要的存储方式都有两种:邻接矩阵和邻接表。前者属于图的顺序存储结构,后者属于图的连接存储结构。

    邻接矩阵法

           所谓邻接矩阵存储,就是用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间的邻接关系的二维数组称为邻接矩阵。
           结点数为n的图\(G=(V,E)\),的邻接矩阵A是\(n*n\)的。将G的顶点编号为\(v_1,v_2,v_3,\ldots,v_n\)。若\((V_i,V_j)\in E\),则\(A[i][j]=1\),否则\(A[i][j]=0\)
    《数据结构-王道-图》

           对于带权图而言,若顶点\(v_i\)\(v_j\)之间有边连接,则邻接矩阵中对应项存放着改变对应的权值,若顶点\(v_i\)\(v_j\)不相连,则用\(\infty\)来报表时这两个顶点之间不存在边。
    《数据结构-王道-图》

           图的邻接矩阵存储结构定义如下:

#define MaxVertexNum 100 
typedef char VertexType;
typedef int EdgeType;
typedef struct
{
    VertexType Vex[MaxVertexNum]; // 顶点表,用于存储途中的所有顶点
    EdgeType EDge[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表。
    int vexnum,arcnum;//图中当前顶点数和弧数。
}MGragh;

注意:在简单应用中,可以直接用二维数组作为图的邻接矩阵(顶点等信息均可忽略)。
当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义值为0和1的枚举类型。
无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。
邻接矩阵表示法的空间复杂度为\(O(n^2)\),其中n为图的顶点数\(|V|\)

图的邻接矩阵存储表示法具有以下特点:
  1. 无向图的邻接矩阵一定是一个对称矩阵(并且唯一)。因此在实际存储邻接矩阵时只需要存储上(或下)三交矩阵的元素即可。
  2. 对于无向图,邻接矩阵的第i行(或第i列)非零元素的个数正好是第i个顶点的度\(TD(V_i)\)
  3. 对于有向图,邻接矩阵的第i行(或第i列)非零元素的个数正好是第i个顶点的出度\(OD(V_i)\)(或入度\(ID(V_i)\))。
  4. 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行,按列对每个元素进行监测,所花费的时间代价很大。这是用邻接矩阵存储图的局限性。
  5. 稠密图适合用邻接矩阵存储表示。
  6. 设图G的邻接矩阵为A,\(A^n\)的元素\(A^n[i][j]\)等于由顶点i到顶点j的长度为n的路径的数目,该结论了解即可,证明方法在离散数学中。

    邻接表法

           当一个图为稀疏图时,是用邻接矩阵表示法显然浪费了大量的存储空间。而图的邻接表法结合了顺序存储和链式存储的方法,大大的减少了这种不必要的浪费。
           所谓邻接表就是对图G中的每个顶点\(V_i\)建立一个单链表,第i个单链表中的结点表示依附于顶点\(V_i\)的边(对于有向图则是以顶点\(V_i\)为尾的弧),这个单链表就成为顶点\(V_i\)的边表(对有向图来说是出边表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点;顶点表结点和边表结点。

    《数据结构-王道-图》

           顶点表结点由顶点域(data)和指向第一条邻接边的指针构成,边表结点由临接点域和指向下一条临界边的指针域构成。
    《数据结构-王道-图》

    图的邻接表存储结构定义如下:

    #define MaxVertexNum 100 
    typedef char VertexType;
    typedef struct ArcNode // 边表结点
    {
     int adjvex;        // 该弧所指向的顶点的位置。
     struct ArcNode *next;// 指向下一条依附于该顶点的弧的指针。
    }ArcNode;
    typedef struct VNode   // 顶点表结点
    {
     VertexType data;   // 顶点信息
     ArcNode *first;    // 指向依附于该顶点的弧的指针
    }VNode,AdjList[MaxVertexNum];
    typedef struct
    {
     AdjList vertices;  // 邻接表
     int vexnum,arcnum; // 图的顶点数和弧数
    }ALGraph;

           图的邻接表存储方法具有一下特点:

  7. 如果G为无向图,则所需的存储空间为\(O(|V|+2|E|)\);如果G为有向图,则所需的存储空间为\(O(|V|+|E|)\)。前者的倍数2是由于无向图中,每条边在邻接表中出现了两次。
  8. 对于稀疏图,采用邻接表表示将极大的节省存储空间。
  9. 邻接表中,给定一顶点,能很容易的找到它的所有临边,因为只需要读取它的邻接表就可以了。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间是\(O(n)\)。但是如果要确定给定的两个顶点间是否存在边,则在邻接矩阵里可以立即查到在邻接表中则需要在相应结点对应的边表中查找另一节点,效率较低
  10. 在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中结点个数即可;但求其顶点的入度,则需要遍历全部的邻接表。因此也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。当然这实际上与邻接表的存储方式是类似的。
  11. 图邻接表表示并不唯一,这是因为在每个顶点对应的单链表中,各边结点的链接次序可以任意,取决于建立邻接表的算法以及边的输入次序。

    十字链表

           十字链表是有向图的一种链式存储结果。在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点。这些节点的结构如下:

弧结点

tailvex

顶点结点

data

弧结点中有5个域:其中尾域(tailvex)和头域(headvex)分别只是弧尾和弧头这两个顶点在图中的位置,链域hlink指向弧头相同的下一条弧,链域tlink指向弧尾相同的下一条弧,info域指向该弧的相关信息。这样弧头相同的弧在同一个链表上,弧尾相同的弧也在同一个链表上。
顶点域中有三个域:data域存放顶点相关的数据信息,如顶点名称,firstin和firstout两个域分别指向以该顶点为弧头和弧尾的第一个弧结点。

《数据结构-王道-图》

#define MaxVertexNum 100
typedef char VertexType;
typedef struct ArcNode
{
    int tailvex,headvex;
    struct ArcNode *hlink,*tlink;
}ArcNode;
typedef struct VNode
{
    VertexType data;
    ArcNode *firstin,*firstout;
}VNode;
typedef struct
{
    VNode xlist[MaxVertexNum];
    int vexnum,arcnum;
}GLGraph;

邻接多重表

       邻接多重表是无向图的另一种链式存储方式。
       在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边,或需要对边执行删除等操作时,需要分别在两个顶点的边表中遍历,效率比较低。
       与十字链表类似,在邻接多重表中,每条边用一个结点表示,其结构如下图所示。
       与十字链表类似,在邻接多重表中每一条边用一个结点表示,其结构如下图。
|mark|ivex|ilink|jvex|jlink|info|
|:-|

       其中,mark为标志域,可用以标记该条边是否被搜索过;ivex和jvex为该边衣服的两个顶点在图中的位置;ilink指向下一条依附于顶点ivex的边;jlink指向下一条依附于顶点jvex的边,info为指向和边相关的各种信息的指针域。

十字链表

       每个顶点也有一个结点表示,它由如下所示的两个域组成。
|data|firstedge|
|:-:|:-:|

       其中,data域存储该顶点的相关信息,firstedge域指示第一跳依附于该顶点的边。
       在邻接多重表中,所有依附于同一顶点的边串联在同一链表中,由于每条边依附于两个顶点,则每个边结点同时连接在两个链表中。

《数据结构-王道-图》

#define MaxVertexNum 100
typedef char VertexType;    //图中顶点数目最大值 
typedef struct ArcNode      //边表结点
{
    bool mark;              //访问标记
    int ivex,jvex;          //分别指向该弧的两个结点
    struct ArcNode *ilink,*jlink;//分别指向两个顶点的下一条边
}ArcNode;
typedef struct VNode        //顶点表结点
{
    VertexType data;        //顶点信息
    ArcNode *firstedge;     //指向第一跳依附该顶点的边
}VNode;
typedef struct
{
    VNode adjmulist[MaxVertexNum];//邻接表
    int vexnum,arcnum;      //图中的顶点数和弧数
}AMLGraph;                  //AMLGraph

图的遍历

       图的遍历是指从图中的某一顶点出发,按照某种搜索方式沿着途中的边对图中所有顶点访问一次且仅访问一次。注意到树是一种特殊的图,所以树的遍历实际上也可以看做是一种特殊的图的遍历。图的遍历是图的一种基本的操作,其他许多操作都建立在图的遍历操作基础之上。
       图的遍历主要有两种算法:广度优先搜索和深度优先搜索。

广度优先搜索(Breadth-First-Search)

       广度优先搜索(BFS)类似于二叉树的层序遍历算法,它的基本思想是:首项访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点\(w_1,w_2,w_3,\ldots,w_i\),然后依次访问它们所有未被访问过的邻接顶点……以此类推,知道所有顶点都被访问过位置。类似的思想还将应用于Dijkstra单源最短路径算法和prime最小生成树算法。
       广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批结点,不想深度优先搜索那样有回退的情况。因此他不是一个递归的算法,为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。

广度优先搜索伪代码

#define MaxVertexNum 100
void BFSTraverse(Graph G)
{
    for(int i=0;i<G.vexnum;i++)// 遍历一下所有的 顶点
        visited[i] = false;    // 将所有的顶点都  设为 未访问标志。
    InitQueue(Q);
    for(int i=0;i<G.vexnum;i++)// 从0号顶点开始遍历
        if(!visited[i])        // 看看有没有访问过,
            BFS(G,i);
}
void BFS(Graph G,int v)
{
    visit(v);
    visited[V]=true;
    EnQueue(Q,v);
    while(isEmpty(Q))
    {
        Dequeue(Q,v);
        for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))
            if(!visited[w])
            {
                visit(w);
                visited[w]=true;
                EnQueue(Q,w);
            }
    }
}
BFS复杂度分析:
1. 不论是邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列Q,n个顶点均需入队一次,在最坏的情况下,空间复杂度为$O(|V|)$。
2. 当采用邻接表存储方式时,每个顶点均需要搜索一次(或者入队一次)姑时间复杂度为$O(|V|)$,在搜索任意一顶点的临接点时,每条边需要访问一次,故时间复杂度为$O(|E|)$。算法的总时间复杂度为$O(|V|+|E|)$。当采用邻接矩阵存储方式时,查找每个顶点的临接点所需的时间为$O(|V|)$,故算法的时间复杂度为$O(|V|^2)$。

BFS算法求解单源最短路径问题

       如果图\(G=(V,E)\)为非带权图,定义从顶点u到顶点v的最短路径\(d(u,v)\)为从u到v的任何路径中最少的边数;如果没有通路,则为\(d(u,v)=\infty\)
       使用BFS,我们可以求解一个满足上述定义的非带权路径的单源最短路径问题,这是由广度优先搜索总是按照距离由近到远来遍历图中每个顶点的性质决定的。
       BFS算法求解单源最短路径问题的算法如下:

void BFS_MIN_Distance(Graph G,int u)
{
    for(int i=0;i<G.vexnum;i++)
        d[i]=INT_MAX;
    visited[u]=true;
    d[u]=0;
    EnQueue(Q,u);
    while(!IsEmpty(Q))
    {
        DeQueue(Q,u);
        for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w))
        {
            if(!visited[w])
            {
                visited[w]=true;
                d[w]=d[u]+1;
                EnQueue(Q,w);
            }
        }
    }
}

深度优先搜索(Depth-First-Search)

       与广度优先搜索不同,深度优先搜索\((DFS)\)类似于树的先序遍历。正如其名称中所暗含的意思一样,这种搜索算法所遵循的策略是尽可能“深”的搜索一个图。它的基本思想如下:首先访问图中某一起始顶点v,然后从v出发,访问与v邻接且未被访问的任一定点\(w_1\),再访问与\(w_1\)邻接且未被访问的任意顶点\(w_2\),……重复上述过程。当不能再继续向下访问时,一次退回到最近被访问的顶点,若他还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,知道搜索顶点均被访问过为止。
       一般情况下,其递归形式的算法非常简洁。下面描述其算法过程。

#define MAX_VERTEX_NUM 100
bool visited[MAX_VERTEX_NUM];
void DFSTraverse(Graph G)
{
    for(v=0;v<G.vexnum;i++)
        visited[v]=false;
    for(v=0;v<G.vexnum;i++)
        if(!visited[v])
            DFS(G,v);
}
void DFS(Graph G,int v)
{
    visit(v);
    visited[v]=true;
    for(w=FirstNeighbor(G,v);w>=0;w=NextNeighor(G,v,w))
        if(!visited[w])
            DFS(G,w);
}

DFS算法性能分析

  1. DFS算法是一个递归算法,需要借助一个递归工作栈,故她的空间复杂度为\(O(|V|)\)
  2. 遍历图的过程实质上是对每个顶点查找其临接点的过程,其耗费的时间取决于所采用的存储结构。当以邻接表进行表示时,查找每个顶点的临接点所需时间为\(O(|V|)\),故总的时间复杂度为\(O(|V|^2)\)。当以邻接表表示时,查找所有顶点的临接点所需时间为\(O(|E|)\),访问顶点所需时间为\(O(V)\),此时,总的时间复杂度为\(O(|V|+|E|)\)

    上面的代码BFSTraverse和DFSTraverse中添加了第二个for循环,再选取初始点,继续进行遍历,以防止一次无法遍历图中的所有顶点。

图的应用

       本节是历年考察的重点。图的应用主要包括:最小生成树,最短路径,拓扑排序和关键路径。一般而言,这部分内容直接以算法设计题形式考查的可能性很小,而更多的是结合图的实例来考查算法的具体执行过程。此外,还需要掌握对于给定的模型建立相应的图去解决问题。

最小生成树(Minimum-Spaning-Tree)

       一个连通图的生成树是图的极小连通子图,它包含图中所有顶点,并且只包含极可能少的边。这意味着对于生成树来说,若砍去它的一条边,就会是生成树变成非连通图;若给它增加一条边,就会形成图中的一条回路。
       对于一个带权连通无向图\(G=(V,E)\),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能同。
       设R为G的所有生成树的集合,若T为R中边的权值之和最小的那棵生成树,则称T为G的最小生成树。

不难看出,最小生成树具有如下性质:
1. 最小生成树不是唯一的,即最小生成树的树形不唯一,R中可能有多个最小生成树。当图G中各边权值互不相等时,G的最小生成树是唯一的;若无向连通图G的边比顶点数少1,即G本身就是一棵树,G的最小生成树就是其本身。
2. 最小生成树的边的权值之和总是唯一的,虽然最小生成树不唯一,但其对应的边的权值之和是唯一的,而且是最小的。
3. 最小生成树的边数为顶点数减 1。

       构造最小生成树有多种算法,但大多数算法都利用了最小生成树的下列性质:
       假设\(G=(V,E)\)是一个带权连通无向图,U是顶点集V的一个非空子集。若\((U,V)\)是一条具有最小权值的边,其中\(u\in U,v\in {V-U}\),则必存在一棵包含边(u,v)的最下生成树。

Prime算法

随意选一个点,开始建立该点集,和其他点的路径长度关系,有路径就有没有的话 设置一个\(\infty\),然后开始在路径的集合里面寻找最短的到达一个点的路径,将这个点加到该点集之中。然后因为加入了新的点,这个时候点集就改变了,我们需要更新一下新的点集到其余点的路径,然后再次选出一个该点集到没有加入该点集的点的最短的路径的一个点。然后就这样一直搞。

KruSkal算法

按照路径的长度进行从小到大的排序,排序完毕之后,选出最小的一条边,作为当前长度。然后选出第二小的边,检查是否会成环,不会的话把长度加起来,然后选第三小的边,检查是否会成环,不会的话把长度加起来,然后选第四小的边。。。。知道跳出来 顶点-1条边。
以前做的《布线问题》作为例题,来解释Prime和Kruskal。

最短路径

Dijkstra算法

和Prime是一样的,不过Prime是最小生成树,计算的是将这些点连起来花费的最小代价。而Dijkstra计算的是,从某点开始到其他点花费的最小代价。
假设从1开始出发,选取一个目前1到那个点最近的点,将该点标记为已访问,然后从1头过这个点我到达其他点的距离会不会更近,如果更近的话,更新一下距离数组。然后从距离数组中选取出另一个未被加入的点,并且是距离1距离最小的点,假设让1通过该点到达其他点会不会更近,如果更近的话更新一下距离数组。

Floyd算法

多源最短路径:核心代码如下
    for(k=1;k<=n;k++)    //Floyd核心算法...
    {             
        for(i=1;i<=n;i++)       //  所有的 路 都让   k  加进去试试  
        {
            for(j=1;j<=n;j++)      //如果  从  i到j的路上 有k 走的会更轻松的话 , 那就让 k 去吧 
            {
                if(e[i][j]>e[i][k]+e[k][j])      //   判断 是否会 更加轻松       
                    e[i][j]=e[i][k]+e[k][j];
             }
        }
     }
三层for循环,如果从i到j路过k的话更快就更新一下距离数组。

拓扑排序

       有向无环图:一个有向图中不存在环,则称为有向无环图,简称DAG图。
       AOV网:如果用DAG图表示一个工程,其顶点表示活动,用有向边\(<V_i,V_j>\)表示活动\(V_i\)必须先于活动\(V_j\)进行的这样一种关系,则这种有向图称为顶点表示活动的网络记为AOV网。在AOV网中,活动\(V_i\)\(V_j\)的直接前驱,活动\(V_j\)\(V_i\)的直接后继,这种前驱和后继关系具有传递性,且任何活动\(V_i\)不能以它自己作为自己的前驱或后继。
       拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序。

  1. 从DAG图中选择一个没有前驱的顶点并输出。
  2. 从图中删除该顶点和所有以它为为起点的有向边。
  3. 重复1和2直到当前的DAG图为空或当前图中不存在无前驱的顶点为止。而后一种情况则说明有向图中必然存在环。
           拓扑序:如果图中有从v到w有一条有向路径,则v一定排在w之前。满足此条件的顶点序列称为一个拓扑序。
           获得一个拓扑序的过程就是拓扑排序。
           \(AOV\)如果有合理的拓扑序,则必定是有向无环图\((Directed Acyclic Graph,DAG)\)
    《数据结构-王道-图》

这个东西就意味着V在开始之前就必须结束。

void TopSort()
{
    for(cnt=0;cnt<|V|;cnt++)
    {
        V=未输出的入度为0的顶点;
        if(这样的V不存在)
        {
            Error("图中有回路");
            break;
        }
        输出V,或者记录V的输出序号;
        for(V的每个临接点W)
        {
            Indegree[W]--;// 每个临接点的入度-1。
        }
    }
}
void TopSort()
{
    for(图中每个顶点V)
        if(InDegree[V]==0)
            EnQueue(V,Q);
    while(!IsEmpty(Q))
    {
        V=DeQueue(Q);
        输出V,或者记录V的输出序号;
        cnt++;
        for(V的每个临接点W)
            if(--Indegree[W]==0)
                EnQueue(W,Q);
    }
    if(cnt!=|V|)
        Error("图中有回路");
}

关键路径

       在带权有向图中,以顶点表示事件,有向边表示活动,边上的权值表示完成活动需要的开销,则这种图称为\(AOE\)网。
       \(AOE\)网具有以下两个性质:

  1. 只有在某顶点所代表的时间发生后,从该顶点出发的各有向边所代表的活动才可以进行;
  2. 只有在进入某一顶点的各有向边,所代表的活动都已经结束时,该顶点所代表的事件才可以发生。
           在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;网中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。
           在AOE网中有些活动是可以并行进行的,从源点到汇点的有向路径可能有多条,并且这些路径长度可能不同。完成不同路径上的活动所需时间虽然不同,但是只有所有路径上的活动都完成了,整个工程才能算结束了。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,我们将关键路径上的活动称为关键活动
           完成整个工程的最短时间就是关键路径的长度,也就是关键路径上各活动花费开销的总和。这是因为关键活动影响了整个工程的时间,即如果关键活动不能按时完成的话,整个工程的完成时间就会增长。因此只要找到了关键活动就找到了关键路径也就可以得出最短完成时间

事件\(V_k\)的最早发生时间VE(K)
       他是指从最早开始顶点V到\(V_k\)的最长路径长度。事件的最早发生时间决定了所有人从\(V_k\)开始的活动最早能够开工的最早时间。
时间\(V_k\)的最早发生时间\(VE(k)\)
       它是指从开始顶点V到\(V(k)\)的最长路径长度。时间的最早发生时间决定了所有从\(V_k\)开始的活动能够开工的最早时间。可以用下面的递推公式进行计算。

\(ve(源点)=0\)
\(ve(k)=Max\{ve(j)+Weight(v_j,v_k)\}\)\(Weight(v_j,v_k)\)表示\(<v_j,v_k>\)上的权值。

时间\(v_k\)的最迟发生时间\(vl(k)\)
       它是指在不推迟整个工程完成的前提下,即保证他所指向的事件\(v_i\)\(ve(i)\)时刻能够发生时,该事件最迟必须发生的时间。

活动\(a_i\)最早开始时间\(e(i)\)
它是指该活动的七点所表示的事件最早发生时间。如果边\(<v_k,v_j>\)表示活动\(a_i\),则有\(e(i)=ve(k)\)

活动\(a_i\)的最迟开始时间。
它是指该活动的终点所表示的事件最迟发生时间与该活动所需时间之差。

一个活动\(a_i\)的最迟开始时间\(l(i)\)和其最早开始时间\(e(i)\)的差额\(d(i)=l(i)-e(i)\)
它是指该活动完成的时间余量,是在不增加整个工程所需的总时间的情况下,活动\(a_i\)可以拖延的时间。如果一个活动的时间余量为0时,说明该活动必须要如期完成,否则就会拖延完成整个工程的进度,所以成\(l(i)-e(i)=0\),即\(l(i)=e(i)\)的活动\(a_i\)是关键活动。

求关键路径的算法步骤如下:(事件是结点,活动是边。)

  1. 求AOE网中所有事件的最早发生时间\(ve()\)
  2. 求AOE网中所有事件的最迟发生时间\(vl()\)
  3. 求AOE网中所有活动的最早开始时间\(e()\)
  4. 求AOE网中所有活动的最迟开始时间\(l()\)
  5. 求AOE网中所有活动的差额\(d()\),找出所有\(d()=0\)的活动构成关键路径。

    《数据结构-王道-图》

    上图中的关键路径就是\(v_1,v_3,v_4,v_6\)

    《数据结构-王道-图》

           对关键路径,我们需要注意以下几点。

  6. 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可以通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定程度,该关键活动就可能变成非关键活动了。
  7. 网中的关键路径并不唯一。且对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。

    原文作者:X-POWER
    原文地址: https://www.cnblogs.com/A-FM/p/9688149.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞