图的相关术语
- 有向图的强连通:表示在有向图中,两个顶点在有向图都有可到达的路径,并不一定是两个顶点之间直接的路径。
例如在图1中,A,B两点是强连通的,A是可以直接到B的,虽然B是不能直接到A的,但是可以通过BCA到达A,所以AB两点是强连通的。强连通图就是在一个有向图中,任意两个顶点之间都是强连通的,所以n个顶点的强连通图,至少需要n条有向边来连接(形成回路即可)。
-
生成树:在一个有向图中,生成树必须包含所有的顶点,同时用尽可能最少的边将这些顶点连接。一个n个顶点的无向图至少需要n-1条边实现生成树。
-
最小生成树:每条边是带权的,在生成树中每条边的权值是最小的。
-
连通分量:图的所有极大连通子图,极大连通子图就是尽可能包含多的顶点和边。连通分量是图的子图,并且是连通子图,并且是尽可能最大的连通子图。
如上图的连通分量有 FGH,IJ,ABCD三个连通分量 。
- 生成森林
在非连通图中,每个连通分量的生成树构成了非连通图的森林。
- 带权路径长度:两个点之间的路径权重之和。
- 几种特殊形态的图
- 无向完全图,无向图中任意两个顶点都存在边。若n个顶点的图为无向图,则需要的边为 Cn2 条。
Cn2 计算公式
- 有向完全图
有向完全图:有向图中任意两个顶点都存在直接的两条弧。那么n个顶点的有向完全图至少2Cn2条边。
-
稀疏图和稠密图
-
树
如果一个图是树,说明肯定是连通的且没有回路。
- 森林
一个或多个树组成的图就是森林。
- 有向树
一个顶点的入度为0,其余顶点的入度均为1的有向图为有向树。
图的存储结构
一、邻接矩阵表示法
- 邻接矩阵法 (适合存储稠密度的图,否则空间复杂度很高,空间浪费)
邻接矩阵法使用一个二维数组存储顶点与边的关系,如6个顶点的无向图,可以使用一个长度为6的二维数组表示,下标0表示第1个顶点
table[0][0],table[0][1],table[0][2],table[0][3],table[0][4],table[0][5],依次表示第1个顶点与其他顶点的边,如果值为1,则表示该顶点与这个顶点有边,值为0,则没有边。值得注意的是,无向图的边的无向的,例如若a[0][2] = 1,则必定a[2][0]等于1。如果求一个顶点边的个数,需要遍历这个顶点,看那个值为1.时间复杂度为o(n)。
邻接矩阵表示有向图:也是使用二维数组,只不过table[0][0],table[0][1],table[0][2],table[0][3],table[0][4],table[0][5],表示了第1个节点的出度,如果值为1,则表示该节点出度。在有向图中统计一个顶点的度需要统计这个点的入度和出度。
- 邻接矩阵法的性质
设图G的邻接矩阵为A,则An的元素An[i][j]等于由顶点i到顶点j的长度为n的路径的数目。例如A2[1][4] = 1,表示从顶点1到顶点4长度为2的路径总共有1条。
二、邻接表表示法
- 邻接表表示法
邻接表表示法主要是用一个一位数组实现,一位数组的下标充当顶点,而数据里面保存的都是结构体类型,结构体保存了当前顶点的含义以及保存的边。
#define MAXSIZE 10
typedef struct Node{
int data;
int nums[MAXSIZE];//每个顶点保存的边
}ANode;
struct TableNode{
ANode table[MAXSIZE];
};
邻接表对比邻接矩阵在空间复杂度上节省了一定的空间复杂度,表示方式是不唯一的,邻接矩阵的表示方式是唯一的。同时,如果是有向图的邻接表,那么在计算一个顶点的度时不方便。适合存储稀疏图。邻接矩阵适合存储稠密图。此外还要注意的是,有向图中某个顶点的邻接表保存的是其出度。
三、十字链表法(存储有向图,不常考)
四、邻接多重表 (存储无向图,不常考)
图的基本操作
-
Adjacent(G,x,y):判断图G是否存在边(x,y)或<x,y>(弧)。
-
Neighbors(G,x):列出图G中与节点x邻接的边。(求出x的邻接表)
如果是邻接矩阵,则依次遍历顶点x即可,时间复杂度O(n),如果是邻接表,最坏时间复杂度也是O(n) 或者 O(|v|)。如果是有向图,找入边的话,需要将每个顶点都遍历,此时的时间复杂度为O(|E|),|E|为边的条数。
- insertVertex(G,x):在图G中插入顶点x
邻接矩阵中插入一个顶点,需要初始化一个下标,时间复杂度为o(1),而邻接表插入一个顶点也需要在一位数组中追加一个元素,时间复杂度也是O(1)。
- DeleteVertex(G,x)删除某一个顶点x
对于邻接矩阵来说,删除一个元素需要把这个元素对应的十字左边全部删除,此时可以在结构体中增加一个变量,表示这个节点是否已被删除。
邻接表删除无向图,首先需要把节点删除,再依次遍历所有子节点,删除等于这条边的节点。时间复杂度为O(1)~O(N).
- AddEdge(G,x,y) 往图中添加一条边
如果图中边不存在,则添加,对于邻接矩阵,添加的时间复杂度为o(1),如果是邻接表的头插法,时间复杂度也是o(1)。
- FirstNeihbor(G,x):求图中顶点G的第一个邻接点。,若有则返回顶点号,若没有邻接点或不存在x,则返回-1.
对于邻接矩阵来说,该算法的时间复杂度为o(1)~o(n),对于邻接表来说,直接返回邻接表的头结点即可。
-
GetEdgeValue(G,x,y) 获取图G中,x与y这两条边的权值。
-
SetEdgeValue(G,x,y,v) 设置图G中,x与y这两条边的权值。
图的遍历
- 广度优先遍历
图的广度优先遍历与树的广度优先遍历差不多,都需要借助一个辅助队列,只不过图的广度优先遍历还需要借助一个辅助标记数组来标记当前节点是否被访问过了。如果当前节点已经被访问过了,则不需要访问且不需要加入到待访问队列,因为无向图的边是具有双向性的。
Java代码实现无向图以及广度优先遍历
/**
* 无向图数据结构
*/
public class WuGraph {
private List<Integer>[] tables = null;
//顶点的数量
private int v;
//边的数量
private int e;
public int getE() {
return e;
}
public WuGraph(int v) {
this.tables = new List[v + 1];
//初始化每个顶点的邻接表
for (int i = 0; i < this.tables.length; i++) {
this.tables[i] = new ArrayList<>();
}
this.v = v;
this.e = 0;
}
//添加一条边
public void addEdge(int x, int y) {
List<Integer> table = tables[x];
if (table.contains(y)) {
return;
}
//将y点添加到x点的邻接表
tables[x].add(y);
//将x点添加到y点的邻接表
tables[y].add(x);
this.e++; //边的数量+1
}
//获取某个顶点邻接表
public List<Integer> getTables(int x) {
return tables[x];
}
//判断某两个顶点之间是否存在边
public boolean hasEdge(int x, int y) {
return tables[x].contains(y);
}
//广度优先遍历
public void bfs(int x) {
//从某个顶点开始,找出这个顶点的邻接表,依次遍历这个顶点的邻接表,遍历完了以后,继续遍历邻接表的第一个子节点的邻接表
//广度优先遍历类似于树的层序遍历,需要借助一个辅助队列,此外由于无向图的一条边是双向关系,所以需要再增加一个额外数组,来标记当前顶点是否被访问过了
boolean[] marked = new boolean[v + 1]; //顶点从1开始
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
//顶点入队
queue.add(x);
while (queue.size() > 0) {
Integer data = queue.poll();
if (marked[data]) {
continue;
}
System.out.println("bfs === " + data);
marked[data] = true;
//获取该点的邻接表
List<Integer> table = tables[data];
for (Integer integer : table) {
//依次将每个邻接表加入队列,这里需要判断是否访问过该顶点了
if (!marked[integer]) {
queue.add(integer);
}
}
}
}
}
测试用例
@Test
public void test1() throws InterruptedException {
WuGraph wuGraph = new WuGraph(8);
//添加边
wuGraph.addEdge(1,5);
wuGraph.addEdge(1,2);
wuGraph.addEdge(2,6);
wuGraph.addEdge(6,3);
wuGraph.addEdge(6,7);
wuGraph.addEdge(3,4);
wuGraph.addEdge(3,7);
wuGraph.addEdge(7,4);
wuGraph.addEdge(7,8);
wuGraph.addEdge(4,8);
wuGraph.bfs(2);
}
上述添加边的操作构建了如下的图
很明显,这个图是连通图,但如果是非连通图,使用上述广度优先算法就无法从一个节点出发遍历完所有顶点。
如上图为非连通图,如果从顶点1或2出发,没有办法遍历完所有节点。
如何解决以上问题?其实很简单,只需要等遍历完成后,再依次遍历标记数组的中剩余没有被标记的元素即可。
升级版的广度优先遍历代码如下:
提到广度优先遍历就想到层序遍历与队列。
private void bfs(int x,boolean[] marked){
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
//顶点入队
queue.add(x);
while (queue.size() > 0) {
Integer data = queue.poll();
if (marked[data]) {
continue;
}
System.out.println("bfs === " + data);
marked[data] = true;
//获取该点的邻接表
List<Integer> table = tables[data];
for (Integer integer : table) {
//依次将每个邻接表加入队列,这里需要判断是否访问过该顶点了
if (!marked[integer]) {
queue.add(integer);
}
}
}
}
//广度优先遍历
public void bfs(int x) {
boolean[] marked = new boolean[v + 1]; //顶点从1开始
bfs(x,marked);
//继续依次遍历剩余未被标记的节点
for (int i = 1; i < marked.length; i++) {
if (!marked[i]){
bfs(i,marked);
}
}
}
广度优先遍历的空间复杂度主要依赖于队列的空间开销,如果一个顶点恰好连接了所有顶点,那么空间复杂度就是O(n)。对于广度优先遍历的时间复杂度来说,主要分析访问各个顶点以及各个顶点的边的时间复杂度即可。不需要研究深层次的for循环。
- 广度优先生成树
由于广度优先遍历也是类似于树的层序遍历,所以一个图采用广度优先遍历,根据各个节点的遍历顺序可以生成一个广度优先生成树。
如上图若是以2为顶点开始广度优先遍历,则可能生成的广度优先生成树如下:
先访问2节点,1,6节点都是从2节点遍历的,所以1,6是2的子节点,继续递归看1的子节点与6的子节点,最终得出此广度优先生成树。值得注意的是,广度优先生成树并不是唯一的,是根据邻接表每个顶点的顺序得来的。
- 广度优先生成森林,与广度优先生成树相关联的就是广度优先生成森林,广度优先生成森林也是从之前的标记数组开始,依次遍历每个顶点生成的每个广度优先生成树。
二、有向图的遍历
/**
有向图数据结构
*/
public class YouGraph {
private List<Integer>[] tables;
private int e; //边的数量
private int v;//顶点的数量
public YouGraph(int v){
this.tables = new List[v + 1];
for (int i = 0; i < this.tables.length; i++) {
this.tables[i] = new ArrayList<>();
}
this.e = 0;
this.v = v;
}
public int getE() {
return e;
}
//添加边,有向图的边是确定的,给顶点x的邻接表添加y
public void addEdge(int x,int y){
if (tables[x].contains(y)){
return;
}
tables[x].add(y);
this.e++;
}
//获取某个顶点的邻接表
public List<Integer> getTable(int x){
return tables[x];
}
//广度优先遍历图
public void BFSTraverse(int x){
boolean[] marked = new boolean[v + 1];
bfs(x,marked);
for (int i = 1; i < marked.length; i++) {
if (!marked[i]){
bfs(i,marked);
}
}
}
private void bfs(int x,boolean[] marked){
System.out.println("bfs调用了===");
//创建层序遍历队列
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
queue.add(x);
while (queue.size() > 0){
Integer poll = queue.poll();
if (marked[poll]){
continue;
}
System.out.println(poll);
marked[poll] = true;
//获取当前节点的邻接表
List<Integer> table = tables[poll];
for (Integer integer : table) {
if (!marked[integer]){
queue.add(integer);
}
}
}
}
}
- 测试代码
@Test
public void test2() {
YouGraph youGraph = new YouGraph(8);
//添加边
youGraph.addEdge(1,5);
youGraph.addEdge(2,1);
youGraph.addEdge(3,6);
youGraph.addEdge(4,3);
youGraph.addEdge(4,7);
youGraph.addEdge(6,2);
youGraph.addEdge(7,3);
youGraph.addEdge(7,6);
youGraph.addEdge(7,8);
youGraph.addEdge(8,4);
//有向图的广度优先遍历
youGraph.BFSTraverse(1); //对于有向图来说,若从节点1开始,则需要调用bfs 5 次
youGraph.BFSTraverse(8); //从顶点8开始,调用bfs 1 次即可
}
上述代码构建了如下图
如果从1开始,则需要调用4次bfs能遍历完所有节点
如果从8开始,则需要调用1次即可遍历完所有节点,因为从8节点能依次找到所有节点的邻接表
- 深度优先遍历
深度优先遍历和树的先根遍历类似,都是先访问根节点,如果根节点有子节点,则先访问子节点,依次递归,只不过图的遍历依然需要一个标记数组来标记当前节点是否访问过了。
//深度优先遍历,类似于树的先根遍历,先访问根节点,如果根节点有子节点,继续访问子节点。
public void DFS(int x){
//树的深度优先遍历同样需要一个标记数组
boolean[] marked = new boolean[v + 1];
dfs(x,marked);
}
private void dfs(int x, boolean[] marked) {
System.out.println(x); //visit
marked[x] = true;
//邻接表
List<Integer> table = tables[x];
for (Integer integer : table) {
if (!marked[integer]){
dfs(integer,marked);//递归,深度优先
}
}
}
对于深度优先遍历,需要先遍历图的顶点,每个顶点又可看作图的根节点。
深度优先遍历的空间复杂度为O(V),此时为最坏情况也就说假设一个图是线性的,那么就会dfs方法就会递归调用V次,最好的情况是假设一个顶点连接了图中所有其他顶点,此时只需要递归调用2次dfs函数即可。
时间复杂度主要包括了访问顶点以及访问顶点的边的时间开销。
如果是邻接表,则时间复杂度为o(|V|+|E|),如果是邻接矩阵,则时间复杂度为O(V2)。
对于邻接表的存储方式来说:深度优先遍历和广度优先遍历的结果可能不是唯一的。
深度优先遍历 —> 先根遍历 + 标记数组
广度优先遍历 —> 层序遍历 + 标记数组
- 深度优先生成树和深度优先生成森林
同广度优先生成树和森林,都是基于顶点开始,构建顶点与边的关系的树。
广度优先生成树比较矮胖,深度优先生成树比较高瘦。
- 图的遍历与图的连通性
对于无向图来说,进行BFS和DFS次数等于该图的连通分量数。
对于连通图来说,调用BFS或DFS次数只需要1次。
对于有向图来说:如果从某个顶点开始能到达所有顶点,则只需要调用1次DFS/BFS,如果该顶点无法到达任意顶点,则需具体分析。
如果当前有向图是一个强连通图(任意两个顶点之间都能互相到达),则只需1次DFS/BFS即可全部遍历完成。
YY
一、最小生成树的应用
- 带权图的最小生成树
一个连通图可以有多个不唯一的生成树,但是一个非连通图是没有生成树这一说的,非连通图叫生成森林和最小生成森林。最小生成树是研究的带权的连通图。值得一提的,如果一个n个顶点的连通图已经具有了n-1条边,则此时已经为最小连通图了。
两大主流算法为普利姆算法和克鲁斯卡尔算法。
- 普利姆算法
普利姆算法默认一个起始顶点,这个其实顶点自己默认就是一颗最初的生成树,然后找与这个树相连接的最小的边,将这条边对应的顶点加入到生成树中。依次循环遍历,直到将所有的顶点加入到最小生成树中。
普利姆算法的时间复杂度为O(V2)
- 克鲁斯卡尔算法
库鲁斯卡尔算法也是默认所有的顶点都是互不连接的,首先从图中找到一条权重最小的边,然后将这个边对应的两个顶点连接,再依次找最小的边连接,直到所有的顶点都在一个树中位置。
克鲁斯卡尔算法的时间复杂度为O(|E|log2|E|).
用普利姆算法求上图的最小生成树步骤:
假设默认D是起点,与D相连的最短路径的点是E,那么DE加入到了最小生成树中,与DE相连的最小的边有EF,DF,权重都是4,此时把EF加入到了树中,DEF构成了最小生成树,继续找,发现A点是与DEF连接最小的边,此时将A点加入到最小生成树,此时最小生成树包含了DEFA顶点,继续找,将B加入到最小生成树,最后再将3加入到最下生成树,带权路径为 2 +4 + 1 + 5 + 3 = 15.
用克鲁斯卡尔算法求上图的最小生成树步骤:
克鲁斯卡尔算法默认有一个空树,首先找到一条最小的边,FA,将这条边对应的两个顶点加入到最小生成树中,再继续找最小的边,找到了ED,将ED这两个顶点也加入到最小生成树中,再继续找,BC,将BC也加入到最小生成树中,此时FA,BC,ED都是单独的顶点,并没有完全把六个顶点连接,再继续找FA与ED之间的最小边,此时找到了FD,此时AFDE这四个顶点在一个树中。此时只需找最后一条边,让BC与AFDE连接方可将这六个顶点都加入到一颗最小生成树中,此时最短的边只剩BF了。至此,用克鲁斯卡尔算法找最小生成树完成。
二、最短路径应用
如果起始点是固定的,求到达某一个顶点的最短路径为单源最短路径。
- BFS求无权图的单源最短路径
广度优先遍历方式实现无向无权图的最短路径主要过程就是在进行广度优先遍历时,除了辅助数组和队列以外,还需多增加两个数组,一个是deep数组,一个是path数组,path数组记录了当前下标顶点的上一个顶点是,deep数组表示起点到当前顶点经过的路径长度。
public void getPath(int x){
int[] path = new int[v + 1];//存储路径当前顶点的上一个顶点 例如 path[8] = 7,8的父节点是7
int[] deep = new int [v + 1];//存储当前顶点x到其他顶点的层级
path[x] = -1;
deep[v] = 0;
boolean[] marked = new boolean[v + 1];//标记数组,标记某个顶点是否被访问过
//一想到广度优先遍历就要用队列
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
queue.add(x);
while (queue.size() > 0){
Integer data = queue.poll();
if (marked[data]){
continue;
}
System.out.println(data);
marked[data] = true;
//获取当前顶点的邻接表
List<Integer> table = tables[data];
for (Integer child : table) {
if (!marked[child]){
//当前节点的上一个节点应该是data
path[child] = data;
//当前节点的深度应该data的深度 + 1
deep[child] = deep[data] + 1;
queue.add(child);
}
}
}
System.out.println("路径是:" + Arrays.toString(path));
System.out.println("各个节点的深度是:" + Arrays.toString(deep));
}
- 迪杰斯特拉算法求最短路径
迪杰斯特拉算法既可以求有向加权图的最短路径,也可以求无向加权图的最短路径,但是迪杰斯特拉也是解决的单源最短路径。
实现迪杰斯特拉算法的关键还是要有3个数组,一个是final数组(作用就相当于是bfs和dfs中的标记数组),一个是dist数组(记录当前顶点可直接到达的邻接表的顶点的距离),最后一个是path数组(记录到达当前顶点路径上的直接前驱)。
初始化工作:
假设从v0顶点开始出发,此时这3个数组的初始值如下,final[0]=true,表示当前节点已经访问,dist[0] = 0,表示当前节点到v0节点的距离是0,path[0]=-1,表示到达当前节点上的路径上的前驱节点是-1。并且找到当前节点邻接表中的未被标识为true的节点。
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
final | true | false | false | false | false |
dist | 0 | 10 | oo | oo | 5 |
path | -1 | 0 | -1 | -1 | 0 |
第一轮遍历
找到未被标识为true,且距离最短的顶点,将其设置为true,标识该顶点可以确定从宏v0-v4最短路径。再找到v4顶点的邻接表,比较每个顶点从v4过去的话是否比之前确定的路径更短?,如果更短则更新距离,且更新这个更短顶点的path,例如v4的邻接表中有v1这个顶点,从v4到v1是3,再加上v4保存的距离5,一共的话就是8,小于v0直接到达v1。此时就把v1顶点的dist修改为8,并且把path[1] 改为4.
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
final | true | false | false | false | true |
dist | 0 | 8 | 14 | 7 | 5 |
path | -1 | 4 | 4 | 4 | 0 |
第二轮遍历
继续找到final数组未被标识未true,且距离最短的顶点,标识该顶点为true,找到该顶点的邻接表,依次比较未被标识为true,且距离更短的节点,此时找到的应该是v3这个顶点,那v3这个顶点的邻接表是v2(6),v0(7),由于v0已经被标识过了,此时只需看v2的距离,v3到达v2距离是 7(到达v3需要的最短距离) + 6(v3到v2的距离) = 13,与v2顶点之前保存的距离比较,发现小于14,则将v2的dist改为13,且path改为3(从v3顶点经过)。第二轮处理完结果如下:
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
final | true | false | false | true | true |
dist | 0 | 8 | 13 | 7 | 5 |
path | -1 | 4 | 3 | 4 | 0 |
第三轮遍历
继续找到未被标识为true且路径最短的几点,,此时找到了顶点1,将顶点1标识为true,找到顶点1的邻接表,顶点1的邻接表是2,4,但是顶点4已经被标识为true了,此时只能看顶点2,从顶点1到顶点2的距离是 8(顶点1的距离) + 1 (顶点2到顶点1的距离)= 9.那9是要小于13的,此时把顶点2的dist数组改为9,path数组改为1(表示从顶点1过来的)。
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
final | true | true | false | true | true |
dist | 0 | 8 | 9 | 7 | 5 |
path | -1 | 4 | 1 | 4 | 0 |
第四轮遍历
第四轮遍历发现为被表示为true的顶点只剩顶点2,此时只需将顶点2标识为true即可。最终使用迪杰斯特拉算法得到的单源最短路径如下
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
final | true | true | true | true | true |
dist | 0 | 8 | 9 | 7 | 5 |
path | -1 | 4 | 1 | 4 | 0 |
顶点1的dist是8,也就是说从顶点0到顶点1经过的最短路径长度是8,节点是 1,4,0,反过来就是0,4,1,也就是从0出发,先经过4,再到达1.顶点2的dist是9,也就是说从顶点0到顶点2经过的最短路径是9,节点是2,1,4,0,反过来就是0,4,1,2。
- 迪杰斯特拉算法总结
首先一点就是要明白3个数组的作用,再一个就是初始化步骤,把起点标识为true,找到邻接表中的其他顶点,依次填充dist与path。然后依次遍历找到不为true且dist最小的顶点,更新为true,然后找到该顶点的邻接表,再依次比较从该顶点出发到邻接表中顶点的距离,如果距离为默认的无穷,则直接更新,否则比较从当前顶点出发到该顶点的距离是否大于该顶点已经保存的距离,如果大于,则不做操作,如果小于,则更新这个顶点的dist与path,依次循环直到所有顶点为true。
三、拓扑排序
有向无环图,亦称AOV网,一个AOV网可以根据某种算法实现拓扑排序,拓扑排序首先要找到出度为0的顶点,加入到栈中,除去该节点之后,继续搜索出度为0的顶点,依次找出所有顶点,生成最后的拓扑排序。与拓扑排序相对应的是逆拓扑排序,逆拓扑排序的算法与拓扑结构算法相反,逆拓扑排序首先入度为0的,除去入度为0的顶点,继续找下一个入度为0的顶点,依次找到入度为0的顶点。值得注意的是,逆拓扑排序也可以使用深度优先算法实现,只不过深度优先算法需要在递归函数的最后访问顶点
AOV网不一定有唯一的拓扑排序顺序,可能存在多种拓扑顺序,但是每次都找入度为0的顶点是可以的。
上图拓扑排序顺序,找到入度为0的顶点,1,去掉1和1的边之后,2和3成为了入度为0的顶点,假设按照属性,将2加入拓扑排序中,接下来加入3,再加入4,再加入5,最后加入6。最后的拓扑排序是1,2,3,4,5,6,也可以是1,3,2,5,4,6. 总之一个顶点的出度一定要在该顶点之后出现。如3一定要在5之前出现,5一定要在6之前出现。而逆拓扑排序就是图的深度优先算法,只不过是在函数调用结束之前访问节点,上图中逆拓扑排序的顺序可以是6,4,2,5,3,1
四、关键路径(AOE网)
从源点到汇点的有向路径可能有多条,所有路径中,具有最大长度的度路径称为关键路径,关键路径上的所有活动称为关键活动。(这里为什么是最大长度路径呢,这里不要忽略了,不是只有在关键路径上的活动才是要活动,而且所有的点都需要处理,只不过最长路径上的活动所需要的时间包含了同步骤下所需要的时间,这样的话只需要最大时间即可,因为其他的都可以同时进行)(关键路径是一个加权有向图,每个顶点是一个事件,每条边代表一个活动,这里的事件就是一瞬间的事,比如JS中的事件,当什么什么的时候)。