[大话数据结构-读书笔记] 图

阅读目录

1 图的定义

现实中,人与人之间关系就非常复杂,比如我认识的朋友,可能他们之间也互相认识, 这就不是简单的一对一、一对多,研究人际关系很自然会考虑多对多的情况。那就是我们今天要研究的主题—图。图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

图(Graph )是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G ( V,E ),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

对于图的定义,我们需要明确几个注意的地方。

  • 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元 素,我们则称之为顶点。
  • 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。而在图结构中,不允许没有顶点。

线性表中,相邻的数据元素之间具有线性关系;树结构中,相邻两层的结点具有层次关系;而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的

1.1 各种图定义

无向边:若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi , vj)来表示。

无向图:若图中任意两个顶点之间的边都是无向边,则称该图为无向图。

对于下图无向图G1来说,G1=(V1, {E1}),其中顶点集合V1={A,B,C,D};边集合E1={(A,B),(B,C),(C,D),(D,A),(A,C)}。

《[大话数据结构-读书笔记] 图》

有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc),用有序偶对<vi , vj>来表示,vi 称为弧尾(Tail),vj 称为弧头(Head)。

有向图:若图中任意两个顶点之间的边都是有向边,则称该图为有向图。

对于下图有向图G2中,G2=(V2,{E2}),顶点集合(A,B,C,D),弧集合E2={<A,D>,{B,A},<C,A>,<B,C>}。

《[大话数据结构-读书笔记] 图》

简单图:在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。

无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。

有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。

:有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权 (Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。

:这种带权的图通常称为网(Network)。

子图:假设有两个图G=(V,{E})和G’=(V’,{E’}),如果V’∈V且E’∈E,则称G’为G的子图

1.2 图的顶点与边间关系

对于无向图G=(V, E),如果边 (v,v’)∈E,则称顶点 v 和 v’ 互为邻接点(Adjacent),即 v 和 v’ 相邻接。边(v,v’)依附(incident)于顶点v和v’,或者说(v,v’)与顶点v和v’相关联。顶点v的(Degree)是和v相关联的边的数目,记为TD(v)。故边数其实就是各顶点度数和的一半,多出的一半是因为重复两次计数。

对于有向图G=(V,{E}),如果弧<v,v’>∈E,则称顶点v邻接到顶点v’,顶点v’邻接自顶点v。弧<v,v’>与顶点v和v’相关联。以顶点v为头的弧的数目称为v的入度,记为ID(v);以顶点v为头的弧的数目称为v的出度,记为OD(v);顶点v的度为TD(v)=ID(v)+OD(v)。故边数=入度和=出度和

路径:图G=(V, E) 中从顶点 v 到 顶点 v’ 的路径(Path)是一个顶点序列。如果G是有向图,则路径也是有向的。路径的长度是路径上的边或弧的数目。

回路 / 环:第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle)。

简单路径:序列中顶点不重复出现的路径称为简单路径。

简单回路 / 简单环:除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。

1.3 连通图相关术语

在无向图G中,如果从顶点 v 到顶点 v’ 有路径,则称 v 和 v’ 是连通的。如果对于图中任意两个顶点都是相通的,则称G是连通图(Connected Graph)。

无向图中的极大连通子图称为连通分量。连通分量的概念强调:

  • 要是子图;
  • 子图要是连通的;
  • 含有极大顶点数;
  • 具有极大顶点数的连通子图包含依附于这些顶点的所有边。

在有向图中,如果对于每一对 vi , vj ∈V,vi ≠ vj,从 vi 到 vj 和从 vj 到 vi 都存在路径,则称G是强连通图

有向图中的极大强连通子图称作有向图的强联通分量

连通图的生成树是一个极小的连通子图,它含有图中全部的 n 个顶点,但只有足以构成一棵树的 n-1 条边。比如下图的图1是一普通图,但显然它不是生成树,当去掉两条构成环的边后,比如图2或图3,就满足n个顶点n-1条边且连通的定义了。它们都是一棵生成树。

《[大话数据结构-读书笔记] 图》

有向树:如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树。

有向图的生成森林:由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。

《[大话数据结构-读书笔记] 图》

1.4 图的定义与术语总结

图按照有无方向分为无向图有向图。无向图由顶点构成,有向图由顶点和 弧构成。弧有弧尾弧头之分。

图按照边或弧的多少分稀疏图稠密图。如果任意两个顶点之间都存在边叫完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图

图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做,有向图顶点分为入度出度

图上的边或弧上带则称为

图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始 点则称为,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图, 有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量

无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶 点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林

2 图的抽象数据类型

图作为一种数据结构,它的抽象数据类型带有自己特点,正因为它的复杂,运用广泛,使得不同的应用需要不同的运算集合,构成不同的抽象数据操作。我们这里就来看看图的基本操作。

《[大话数据结构-读书笔记] 图》

3 图的存储结构

由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数
据元素在内存中的物理位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。对于图来说,如何对它实现物理存储是个难题,不过我们的前辈们已经解决了,现在我们来看前辈们提供的五种不同的存储结构。

3.1 邻接矩阵

邻接矩阵:图的邻接矩阵存储方式是用两个数组来标示图。一个一位数组存储图顶点的信息,一个二维数组(称为邻接矩阵)存储图中边或者弧的信息。

设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:

《[大话数据结构-读书笔记] 图》

实例如下,下面左图是一个无向图。右图是邻接矩阵表示:

《[大话数据结构-读书笔记] 图》

邻接矩阵代码存储结构的代码如下。

#define MAXVEX 100 /* 最大顶点数,应由用户定义 */
#define INFINITY 65535 /* 用65535来表示oo */
typedef char VertexType; /* 顶点类型应由用户定义  */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */
typedef struct
{
    VertexType vexs[MAXVEX]; /* 顶点表 */
    EdgeType arc[MAXVEX][MAXVEX];/* 邻接矩阵,可看作边表 */
    int numNodes, numEdges; /* 图中当前的顶点数和边数  */
}MGraph;

有了这个结构定义,我们构造一个图,其实就是给顶点表和边表输入数据的过
程。我们来看看无向网图的创建代码。

/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
    int i,j,k,w;
    printf("输入顶点数和边数:\n");
    scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
    for(i = 0;i <G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
        scanf(&G->vexs[i]);
    for(i = 0;i <G->numNodes;i++)
        for(j = 0;j <G->numNodes;j++)
            G->arc[i][j]=INFINITY;  /* 邻接矩阵初始化 */
    for(k = 0;k <G->numEdges;k++) /* 读入numEdges条边,建立邻接矩阵 */
    {
        printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
        scanf("%d,%d,%d",&i,&j,&w); /* 输入边(vi,vj)上的权w */
        G->arc[i][j]=w; 
        G->arc[j][i]= G->arc[i][j]; /* 因为是无向图,矩阵对称 */
    }
}

无向网图的创建代码,时间复杂度为O{n + n2 + e},其中对邻接矩阵Garc的初始化耗费了O(n)的时间。

3.2 邻接表

邻接表:用数组和链表结合的存储方式来标示图的方法称为邻接表。

邻接表处理思路:

  • 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于査找该顶点的边信息。
  • 图中每个顶点V1的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点V1的边表,有向图则称为顶点V1作为弧尾的出边表。

邻接表存储结构:
《[大话数据结构-读书笔记] 图》

邻接表存储代码结构的代码如下。

typedef char VertexType; /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */

typedef struct EdgeNode /* 边表结点  */
{
    int adjvex;    /* 邻接点域,存储该顶点对应的下标 */
    EdgeType info;      /* 用于存储权值,对于非网图可以不需要 */
    struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;

typedef struct VertexNode /* 顶点表结点 */
{
    VertexType data; /* 顶点域,存储顶点信息 */
    EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];

typedef struct
{
    AdjList adjList; 
    int numNodes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;

对于邻接表的创建,也就是顺理成章之事。无向图的邻接表创建代码如下。

/* 建立图的邻接表结构 */
void  CreateALGraph(GraphAdjList *G)
{
    int i,j,k;
    EdgeNode *e;
    printf("输入顶点数和边数:\n");
    scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
    for(i = 0;i < G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
    {
        scanf(&G->adjList[i].data);     /* 输入顶点信息 */
        G->adjList[i].firstedge=NULL;   /* 将边表置为空表 */
    }
    
    
    for(k = 0;k < G->numEdges;k++)/* 建立边表 */
    {
        printf("输入边(vi,vj)上的顶点序号:\n");
        scanf("%d,%d",&i,&j); /* 输入边(vi,vj)上的顶点序号 */
        e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
        e->adjvex=j;                    /* 邻接序号为j */                         
        e->next=G->adjList[i].firstedge;    /* 将e的指针指向当前顶点上指向的结点 */
        G->adjList[i].firstedge=e;      /* 将当前顶点的指针指向e */               
        
        e=(EdgeNode *)malloc(sizeof(EdgeNode)); /* 向内存申请空间,生成边表结点 */
        e->adjvex=i;                    /* 邻接序号为i */                         
        e->next=G->adjList[j].firstedge;    /* 将e的指针指向当前顶点上指向的结点 */
        G->adjList[j].firstedge=e;      /* 将当前顶点的指针指向e */               
    }
}

3.3 十字链表

对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。而十字链表将邻接表与逆邻接表结合起来。还有邻接多重表和边集数组这两种方法,这里就不多做介绍了。

4 图的遍历

定义:从图中某个顶点出发访遍图中其余顶点,且使每个顶点仅被访问依次,这一过程叫做图的遍历。

我们需要在遍历过程中把访问过的顶点打上标记,以避免访问多次而不自知。具体办法是设置一个访问数组visited[n],n是图中顶点的个数,初值为0,访问过后设置为1

4.1 深度优先遍历

也称为深度优先搜索,简称为DFS。

深度优先遍历其实就是一个递归的过程,就像是一棵树的前序遍历,没错,它就是。它从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。

如果我们用的是邻接矩阵的方式,则代码如下:

/* Boolean是布尔类型,其值是TRUE或FALSE */
typedef int Boolean;             
/* 访问标志的数组 */
Boolean visited[MAX];            
/* 邻接矩阵的深度优先递归算法 */
void DFS(MGraph 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] == 1 && !visited[j])
            /* 对为访问的邻接顶点递归调用 */
            DFS(G, j);           
}
/* 邻接矩阵的深度遍历操作 */
void DFSTraverse(MGraph G)
{
    int i;
    for (i = 0; i < G.numVertexes; i++)
        /* 初始所有顶点状态都是未访问过状态 */
        visited[i] = FALSE;      
    for (i = 0; i < G.numVertexes; i++)
        /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
        if (!visited[i])         
            DFS(G, i);
}

对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要O(n2)的时间。

如果图结构是邻接表结构,其DFSTraverse函数的代码是几乎相同的,只是在递归函数中因为将数组换成了链表而有不同,代码如下。

/* 邻接表的深度优先递归算法 */
void DFS(GraphAdjList GL, int i)
{
    EdgeNode *p;
    visited[i] = TRUE;
    /* 打印顶点,也可以其他操作 */
    printf("%c ", GL->adjList[i].data);    
    p = GL->adjList[i].firstedge;
    while (p)
    {
        if (!visited[p->adjvex])
            /* 对为访问的邻接顶点递归调用 */
            DFS(GL, p->adjvex);            
        p = p->next;
    }
}
/* 邻接表的深度遍历操作 */
void DFSTraverse(GraphAdjList GL)
{
    int i;
    for (i = 0; i < GL->numVertexes; i++)
        /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
        if (!visited[i])                   
            DFS(GL, i);
}

而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。对于有向图而言,由于它只是对通道存在可行或不可行,算法上没有变化,是完全可以通用的。

4.2 广度优先遍历

又称为广度优先搜索,简称BFS。

如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了。

我们将下图的第一幅图稍微变形,变形原则是顶点A放置在最上第一层,让与它有边的顶点B、F为第二层,再让与B和F有边的顶点C、I、G、E为第三层,再将这四个顶点有边的D、H放在第四层,如图7-5-3的第二幅图所示。此时在视觉上感觉图的形状发生了变化,其实顶点和边的关系还是完全相同的。

《[大话数据结构-读书笔记] 图》

以下是邻接矩阵结构的广度优先遍历算法。

/* 邻接矩阵的广度遍历算法 */
void BFSTraverse(MGraph G)
{
    int i, j;
    Queue Q;
    for (i = 0; i < G.numVertexes; i++)
        visited[i] = FALSE;
    /* 初始化一辅助用的队列 */
    InitQueue(&Q);                                   
    /* 对每一个顶点做循环 */
    for (i = 0; i < G.numVertexes; i++)              
    {
        /* 若是未访问过就处理 */
        if (!visited[i])                             
        {
            /* 设置当前顶点访问过 */
            visited[i]=TRUE;                         
            /* 打印顶点,也可以其他操作 */
            printf("%c ", G.vexs[i]);                
            /* 将此顶点入队列 */
            EnQueue(&Q,i);                           
            /* 若当前队列不为空 */
            while (!QueueEmpty(Q))                   
            {
                /* 将队中元素出队列,赋值给i */
                DeQueue(&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]);    
                        /* 将找到的此顶点入队列 */
                        EnQueue(&Q,j);               
                    }
                }
            }
        }
    }
}

其中涉及到队列操作:

/* 用到的队列结构与函数********************************** */
 
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */  
 
/* 循环队列的顺序存储结构 */
typedef struct
{
    int data[MAXSIZE];
    int front;      /* 头指针 */
    int rear;       /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}Queue;
 
/* 初始化一个空队列Q */
Status InitQueue(Queue *Q)
{
    Q->front=0;
    Q->rear=0;
    return  OK;
}
 
/* 若队列Q为空队列,则返回TRUE,否则返回FALSE */
Status QueueEmpty(Queue Q)
{ 
    if(Q.front==Q.rear) /* 队列空的标志 */
        return TRUE;
    else
        return FALSE;
}
 
/* 若队列未满,则插入元素e为Q新的队尾元素 */
Status EnQueue(Queue *Q,int e)
{
    if ((Q->rear+1)%MAXSIZE == Q->front)    /* 队列满的判断 */
        return ERROR;
    Q->data[Q->rear]=e;         /* 将元素e赋值给队尾 */
    Q->rear=(Q->rear+1)%MAXSIZE;/* rear指针向后移一位置, */
                                /* 若到最后则转到数组头部 */
    return  OK;
}
 
/* 若队列不空,则删除Q中队头元素,用e返回其值 */
Status DeQueue(Queue *Q,int *e)
{
    if (Q->front == Q->rear)            /* 队列空的判断 */
        return ERROR;
    *e=Q->data[Q->front];               /* 将队头元素赋值给e */
    Q->front=(Q->front+1)%MAXSIZE;  /* front指针向后移一位置, */
                                    /* 若到最后则转到数组头部 */
    return  OK;
}
/* ****************************************************** */

对比图的深度优先遍历与广度优先遍历算法,你会发现,它们在时间复杂度上是一样的,不同之处仅仅在于对顶点访问的顺序不同。可见两者在全图遍历上是没有优劣之分的,只是视不同的情况选择不同的算法。

不过如果图顶点和边非常多,不能在短时间内遍历完成,遍历的目的是为了寻找合适的顶点,那么选择哪种遍历就要仔细斟酌了。深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。

5 最小生成树

所谓的最小成本,就是n个顶点,用n-1条边把一个连通图连接起来,并且使得权值的和最小

一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。那么我们把构造连通网的最小代价生成树称为最小生成树。

最小生成树实现算法:普利姆算法和克鲁斯卡尔算法。具体的算法,这里由于篇幅原因不再说明。

6 最短路径

定义:对于网图来说,最短路径是指两个顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点式源点,最后一个顶点是终点。

两种计算最短路径算法:迪杰斯特拉(Djikstra)算法和佛洛伊德算法。具体的算法,这里由于篇幅原因不再说明。

7 拓扑排序

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图顶点表示活动的网,我们称为AOV网(ActivityON)。AOV网中的弧表示活动之间存在的某种制约关系

设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2,……,vn,满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj之前。则我们称这样的顶点序列为一个拓扑序列。

所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。构造时会有两个结果,如果此网的全部顶点都被输出,则说明它是不存在环(回路)的AOV网;如果输出顶点数少了,哪怕是少了一个,也说明这个网存在环(回路),不是AOV网

对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止

8 关键路径

拓扑排序主要是为解决一个工程能否顺序进行的问题,但有时我们还需要解决工程完成需要的最短时间问题。

我们如果要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就是最短时间

在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网(Activity On Edge Network)。我们把AOE网中没有入边的顶点称为始点或源点没有出边的顶点称为终点或汇点。由于一个工程总有一个开始一个结束,所以正常情况下,AOE网只有一个源点一个汇点

我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。显然就下图的AOE网而言,开始→发动机完成→部件集中到位→组装完成就是关键路径,路径长度为5.5。

《[大话数据结构-读书笔记] 图》

只有缩短关键路径上的关键活动时间才可以减少整个工期长度

9 总结回顾

由于图也是最复杂的数据结构,对它讲解时,涉及到数组、链表、栈、队 列、树等之前学的几乎所有数据结构。因此从某种角度来说,学好了图,基本就等于理解了数据结构这门课的精神。

图的存储结构我们一共讲了五种,其中比较重要的是邻接矩阵和邻接表,它们分别代表着边集是用数组还是链表的方式存储。十字链表是邻接矩阵的一种升级,而邻接多重表则是邻接表的升级。边集数组更多考虑的是对边的关注。 用什么存储结构需要具体问题具体分析,通常稠密图,或读存数据较多,结构修改较少的图,用邻接矩阵要更合适,反之则应该考虑邻接表

图的遍历分为深度广度两种,各有优缺点。

图的应用是我们这一章浓墨重彩的一部分,一共谈了三种应用:最小生成树、最短路径和有向无环图的应用。

最小生成树,我们讲了两种算法:普里姆(Prim)算法克鲁斯卡尔(Kruskal) 算法。普里姆算法像是走一步看一步的思维方式,逐步生成最小生成树。而克鲁斯卡 尔算法则更有全局意识,直接从图中最短权值的边入手,找寻最后的答案。

最短路径的现实应用非常多,我们也介绍了两种算法,迪杰斯特拉(Dijkstra)算法更强调单源顶点査找路径的方式,比较符合我们正常的思路,容易理解原理,但算法代码相对复杂。而弗洛伊德(Fbyd)算法则完全抛开了单点的局限思维方式,巧妙地应用矩阵的变换,用最清爽的代码实现了多顶点间最短路径求解的方案,原理理解有难度,但算法编写很简洁。

有向无环图时常应用于工程规划中,对于整个工程或系统来说,我们—方面关心的是工程能否顺利进行的问题,通过拓扑排序的方式,我们可以有效地分析出一个有向图是否存在环,如果不存在,那它的拓扑序列是什么?另—方面关心的是整个工程完成所必须的最短时间问题,利用求关键路径的算法,可以得到最短完成工程的工期以及关键的活动有哪些。

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