数据结构学习笔记(6)——图
图的存储结构
图的顺序存储结构——邻接矩阵
typedef struct { int no; //顶点编号,表示它的位置char info; //顶点的其他辅助信息,没有的话可以删除
}VertexType; //顶点 typedef struct{ int edges[maxSize][maxSize]; //顶点之间的相邻关系(无权值:1表示相通,0表示不相通)(有权值:∞表示不相通,其它为权值)int vexnum, arcnum; //总顶点数和总边数VertexType vex[maxSize]; //存放图中的所有顶点
}MGraph; //图
图的链式存储结构——邻接链表
//边——结点
typedef struct ArcNode {int adjvex; //该边所指顶点的位置struct ArcNode *nextArc; //指向下一条边的指针int info; //边的补充信息,如权值,没有则省略
}ArcNode;//顶点——结点
typedef struct VNode {char data; //顶点信息ArcNode *firstArc; //指向第一条依附于该顶点边的指针
}VNode;//图的邻接表存储类型
typedef struct AGraph {VNode adjlist[maxSize]; //邻接表,由顶点表结点组成的数组int vexnum, arcnum; //顶点数和边数
}AGraph;
图的遍历
深度优先搜索遍历
类似于二叉树的先序遍历。算法执行过程:访问任一顶点,然后从该顶点出发,递归访问该顶点的所有未被访问的邻接顶点。
int visited[maxSize]; //全局数组,作为顶点的访问标记,取0表示该位置的顶点未被访问,取1表示该位置的顶点未被访问void DFSTraverse(AGraph *g) { //深度优先搜索遍历图g//初始化标记数组for (int v = 0; v < g->vexnum; v++) {visited[v] = 0; }//从0顶点开始遍历(连通图只需要从任一顶点出发就能完成遍历,非连通图需要依次从多个顶点出发才能完成遍历,这里假设为非连通图)for (int v = 0; v < g->vexnum; v++) { if (visited[v] == 0) //对每个连通分量调用一次DFSDFS(g, v); //v位置的顶点未被访问过,访问之}
}void DFS(AGraph *G, int v) { //从顶点v出发,深度优先遍历Gvisit(v); //访问顶点vvisited[v] = 1; //更改全局数组,表示v已经被访问过//递归访问v的所有未被访问过的邻接顶点ArcNode *p = G->adjlist[v].firstArc; //p指向顶点v的第一条边while (p != NULL){ if (visited[p->adjvex] == 0) { DFS(G, p->adjvex); //边p另一端的顶点,即v的邻接顶点没被访问过,递归访问之p = p->nextArc; //挪动p指向v的下一条边,循环访问v的其他邻接顶点}}
}
- 深度优先搜索生成树:对连通图的深度优先搜索遍历过程中所经历的边保留,其余的边删掉,就会形成一棵树,称为~。
- 图的深度优先搜索遍历类似于二叉树的先序遍历。区别在于二叉树的先序遍历对于每个结点要递归地访问两个分支,而图的深度优先搜索遍历则是递归地访问多个分支。
- DFS的空间复杂度:O(n),n为顶点个数。
- DFS的时间复杂度:
- 图以邻接矩阵表示:查找每个顶点的邻接点所需要的时间为O(n),有n个顶点,故总的时间复杂度为O(n2)
- 图以邻接表表示:查找所有顶点的邻接点所需要的是时间为O(e),访问所有顶点为O(n),故总的时间复杂度为O(n+e)
广度优先搜索遍历
类似于二叉树的层次遍历。访问v的所有邻接顶点,再访问v的所有邻接顶点的邻接顶点…以此类推,直到图中所有顶点都被访问。
广度优先搜索需要借助一个队列:
- 任取图中一个结点访问,入队,并标记其已经被访问过
- 队列非空时循环执行:出队,依次检查出队结点的所有邻接顶点,如果没被访问过,访问之,然后入队;
- 队列非空时跳出循环,结束遍历。
int visited[maxSize]; //全局数组,作为顶点的访问标记,取0表示该位置的顶点未被访问,取1表示该位置的顶点未被访问//对图g进行广度优先搜索遍历
void BFSTraverse(AGraph *g) { //广度优先搜索遍历图gfor (int v = 0; v < g->vexnum; v++) {visited[v] = 0; //初始化标记}for (int v = 0; v < g->vexnum; v++) //从v=0开始遍历{if (visited[v] == 0) //对每个连通分量调用一次BFSBFS(g, v); //v位置的顶点未被访问过,访问之 }
}//BFS函数,访问v的所有邻接顶点,再访问v的所有邻接顶点的邻接顶点......
void BFS(AGraph *G, int v) {int que[maxSize], front = 0, rear = 0; //定义循环队列作为辅助工具visit(v); //访问顶点vvisited[v] = 1; //更改访问标记rear = (rear + 1) % maxSize; //顶点v入队que[rear] = v;int j; //两个临时变量,j用来接收出队顶点,p用来接收出队顶点j的第一条边ArcNode *p; //队列非空时执行,完成对v的所有邻接顶点的访问后,再访问v的所有邻接顶点的邻接顶点......while (front != rear) { //队列非空,出队front = (front + 1) % maxSize; j = que[front]; //出队顶点p = G->adjlist[j].firstArc; //出队顶点的第一条边//遍历出队顶点j的所有邻接顶点,未被访问过就访问之,并入队while(p != NULL) { //如果j的邻接顶点p->adjvex未被访问过,访问并入队if (visited[p->adjvex] != 0) {visit(p->adjvex);visited[p->adjvex] = 1;rear = (rear + 1) % maxSize;que[rear] = p->adjvex;}//更新p指向j的下一条边,检查j的下一个邻接顶点p = p->nextArc;}//p的所有邻接顶点访问完之后,队列中存有p的所有邻接顶点,再去访问访问它们的邻接顶点,直到队空退出外层循环}
}
- BFS的时间复杂度:
- 图以邻接矩阵表示:查找每个顶点的邻接点所需要的时间为O(n),有n个顶点,故总的时间复杂度为O(n2)
- 图以邻接表表示:查找所有顶点的邻接点所需要的是时间为O(e),访问所有顶点为O(n),故总的时间复杂度为O(n+e)
判断无向图G是否是一棵树
一个无向图是一棵树的条件是有n-1条边的连通图(n为图中顶点的个数)。
判读无向图g是否是一棵树需要满足两个条件:
- 无向图g是一个连通图;
- 无向图有n-1条边,其中n是顶点数;
//判断无向图是否是一棵树
int G_isTree(AGraph *g) {for (int i = 0; i < g->vexnum; i++) {visited[i] = 0;}int vn = 0, en = 0; //新增两个计数器:vn——已访问顶点数,en——已访问边数DFS_isTree(g, 0, vn, en); //从0顶点出发,DFS图g//判读无向图g是否是一棵树需要满足两个条件://1.无向图g是一个连通图:通过一次DFS返回的顶点数是否和g的顶点数相等//2.无向图有n-1条边,n是顶点数:深度遍历过程中,每条边都会访问2次,所以需要除以2和n-1比较if (vn == g->vexnum && en / 2 == (g->vexnum - 1)) { return 1;}else {return 0;}
}//修改DFS算法,新增两个计数器:vn——已访问顶点数,en——已访问边数
void DFS_isTree(AGraph *G, int v, int &vn, int &en) {vn++; //访问操作改为:已访问顶点数+1visited[v] = 1; ArcNode *p = G->adjlist[v].firstArc; while (p != NULL) {en++; //已访问边数+1if (visited[p->adjvex] == 0) { DFS_isTree(G, p->adjvex, vn, en);p = p->nextArc; }}
}
判断顶点i到顶点j是否连通
方法:从顶点i出发遍历图,如果遇到j则说明连通,否则不连通。
// 判断顶点i到顶点j是否连通
int DFSTrave(AGraph *g, int i, int j) {for (int v = 0; v < g->vexnum; v++){visited[v] = 0;}DFS(g, i);//visited[j]等于1,说明被访问过了if (visited[j] == 1) {return 1;}else{return 0;}
}
图的应用
最小生成树
- 生成树:一个连通图的最小生成树包含图中的所有顶点,并且只含尽可能少的边。
- 砍去生成树的一条边,生成树就会变成非连通图;增加一条边,它就会形成一个回路。
- 最小生成树:带权连通无向图中权值之和最小的生成树。
- 最小生成树不是唯一的,但不同最小生成树的边权值之和是一样的,而且是最小的。当图中各边的权值不相等时,最小生成树唯一。
- 图的边数比它的顶点数少1,即图本身就是一棵树时,它的最小生成树是它自己。
- 最小生成树的边数=顶点数-1。
普里姆(Prim)算法
基本思想:从图中任取一个顶点,把它当做一棵树,然后从与这棵树相接的边中选取一条最短(权值最小)的边,将这条边与其连接的顶点一并加入到树中,重复这个过程,直到图中所有顶点都被并入树中,此时得到的数就是最小生成树。
普里姆算法需要用到两个辅助数组:
- vset[]数组,vset[i]=1表示顶点i已经加入到树中,vset[i]=0说明顶点i还未加入到树中(类似于上面的visited[],标记数组)。
- lowcost[]数组,存放当前生成树到其余顶点最短边的权值。比如lowcost[j]=4就表示当前生成树到j顶点的最短边的权值是4。
要注意,lowcost[]数组中存放的当前生成树到其余顶点的最小权值,而非树中结点到顶点的权值。而且,树到其余顶点的边可能有很多条,此时应该选最短的那一条边,记录它的权值。
从树中任一顶点v0开始,构成最小生成树的算法执行过程:
v0作为树根结点,然后从v0到其他顶点的所有边都作为候选边
重复执行以下步骤n-1次,使得其他n-1个顶点被并入到树中:
(1)从候选边中挑出权值最小的边 ,将与之相连的另一个顶点v并入到树中;
(2)更新lowcost数组,以新加入的顶点v为出发点,如果其余顶点vi使得(v0,vi)的权值比lowcost[vi]小,就用(v0,vi)的权值覆盖lowcost[vi]。
这里采用的图的存储结构是顺序存储结构——邻接矩阵。
#define INF 100 //INF是一个已经定义的比图中所有边权值都大的常量//普里姆算法
void Prim(MGraph g, int v0, int &sum)
{int min, v; //min:记录每次新增顶点后的权值增量;v:树新增顶点int lowcost[maxSize], vset[maxSize]; //遍历图中顶点,给lowcost[], vset[]赋初始值for (int i = 0; i < g.vexnum; i++) { lowcost[i] = g.edges[v0][i]; //开始时,v0自己是一棵树,lowcost数组里放v0到其他边的权值vset[i] = 0; }vset[v0] = 1; //修改v0的标记值,表示v0已经加入到树中sum = 0; //sum用来保存当前生成树的总权值//循环g.vexnum-1次,将除了v0以外的其他顶点加入到树中for (int i = 0; i < g.vexnum-1; i++){min = INF; //INF为已知的比图中所有边的权值都大的常量//1.选出当前生成树到其余顶点的边中权值最小的那一条for (int j = 0; j < g.vexnum; j++) { //vest[j]等于0说明未加入树中,lowcost[j]小于min说明树到顶点j的边权值可能是最小的if (vset[j] == 0 && lowcost[j] < min) {min = lowcost[j]; //不停地刷新min值,遍历结束,min记录选出的边的权值v = j; //v记录顶点}}vset[v] = 1; //选出的顶点加入树sum += min; //刷新当前生成树的总权值//2.每次加入一个新顶点,树到其他顶点的权值信息会发生变化,所以要刷新lowcost[]for (int k = 0; k < g.vexnum; k++){ //从新加入顶点v出发,如果到剩余某个顶点k的权值比lowcost[]中记录的值小,则用其值覆盖lowcost[]if (vset[k] == 0 && g.edges[v][k] < lowcost[k]) {lowcost[k] = g.edges[v][k];}}//完成一个树结点的添加,继续添加下一个}
}
- Prim算法不依赖边,它的时间复杂度为O(n2)。
- 因为不依赖边,Prim算法适用于求解边稠密的图的最小生成树。
克鲁斯卡尔(Kruskal)算法
基本思想:每次找出侯选边中权值最小的边,将其加入到树中。重复这个过程直到所有边都被检测完。
是否为侯选边:看这条边的并入是否会构成回路作为标准。构成回路就说明这条边两边的顶点都是树中结点。
算法执行过程:
将图中所有的边按照权值从小到大排序;
从权值最小边开始扫描各边,若该边为侯选边,则将其加入到树中,直到所有的树都被检测完。
为了判断侯选边的加入是否会构成回路,我们需要借助并查集。并查集类似于树的双亲存储结构,我们定义一个一维数组,用下标表示顶点的编号,用下标对应的数组值表示它的父结点。这种结构有两个好处:
- 可以快速的将两棵树合并为一棵树,只需要找到其中树A的根结点a,a作为树B的任一结点的孩子结点即可。即只需要修改A[a]的值。
- 可以很方便的判断两个结点是否属于用一棵树,只需要知道它们各自的根结点,看是否相等即可。
我们把图的所有顶点放入到并查集中,在算法开始之前,将他们看做一棵棵单独的树,算法执行的过程中,不断地合并两棵树即可。
我们规定,在并查集V[]中,如果i是某棵树根结点,那么A[i]=i,即根结点的双亲结点是它自己。
那么将图g中所有顶点存到并查集v中并初始化的代码应该这样写:
for (int i = 0; i < g.vexnum; i++){v[i] = i;
}
可以看到,与普里姆算法中针对顶点操作不同,克鲁斯卡尔算法中要不断地针对“边"进行操作,所以我们需要定义新的数据结构来保存和边有关的信息:
//Road结构保存了边,权值和它的两个结点
typedef struct {int a, b; //边的两个顶点int w; //边的权值
}Road;
克鲁斯卡尔算法,假设road[]数组中已经存放了图中所有边,且排序函数sort()已经存在:
Road road[maxSize]; //定义road[]存放图中所有的边信息
int v[maxSize]; //定义并查集void kruskal(MGraph g, int &sum, Road road[]) {int a, b;sum = 0;//所有顶点加入并查集并单独作为一棵树,并查集初始化for (int i = 0; i < g.vexnum; i++){v[i] = i;}//对road数组中所有边信息按照权值从小到大排序,这里没有具体实现sort函数sort(road, g.arcnum);//从权值最小边开始扫描各边,若该边为侯选边,则将其加入到树中for (int j = 0; j < g.arcnum; j++){a = getRoot(road[j].a); //得到边j的其中一个顶点a在并查集结构中的根结点b = getRoot(road[j].b); //得到边j的其中一个顶点b在并查集结构中的根结点//a!=b说明边road[j]的两个顶点属于不同的两棵树,则合并这两棵树,将a作为b的子树if (a != b) {v[a] = b;sum += road[j].w; //更新总权值}}
}//getRoot()方法放回并查集v中的根结点
int getRoot(int n) {while (n!=v[n]) //n不是根结点{n = v[n]; //从n出发,沿着它的父结点往上找根结点}return n;
}
Road存储结构示意图:
下图右半部分为算法执行之后的并查集结构以及最小生成树的的结构示意图:
Kruskal算法时间花费在排序函数sort()和单层循环中,循环是线性级的,可以认为算法时间主要花费在排序函数中,根据所选排序函数不同,算法的时间复杂度不同。由于排序函数是以图的边数为问题规模的,与顶点数无关,可见Kruskal算法适合于稀疏图。
- Prim算法和Kruskal算法都是针对无向图的。
- Prim算法适用于稠密图,而Kruskal适用于稀疏图。
最短路径
- 最短路径:当图是带权图时,把一个顶点v0到图中任意一个顶点vi的一条路径(非唯一)所经过的边上的权值之和,定义为该路径的带权路径长度,代全路径长度最短的路径就是最短路径。
迪杰斯特拉(Dijkstra)算法
迪杰斯特拉(Dijkstra)算法主要用来求图中任意顶点v0到其余各顶点的最短路径。
其基本思想是:每次加入的新顶点vj,保证v0从现有路径到vj的路径长度是v0到其他剩余顶点中最短的。每次加入新顶点后,重新计算v0到剩余顶点的最短路径值。
需要用到3个辅助数组:
- dist[ vi]表示当前已找到的从v0到每个终点 Vi的最短路径的长度。它的初态为:若从v0到 Vi有边,则dist[vi]为边上的权值,否则置 dist[vi]为∞(算法里为INF,一个比图中任意边权值都大的常数)。
- path[vi]中保存从 v0到 Vi最短路径上 Vi的前一 个顶点。path[]的初态为:如果 v0到 vi有边,则 path[vi]=v0,否则 path[vi]=-1。
- set[]为标记数组,set[vi]=0表示vi还没有被并入最短路径;set[vi]=1表示 vi已经被并入最短路径。set[]初态为:set[v0]=1, 其余元素全为0。
迪杰斯特拉算法执行过程如下:
从当前dist[]数组中选出最小值加入路径,假设为dist[vu],则需要设置set[vu]=1。
新顶点的加入可能会使得从v0到vk的路径长度变得更短,所以可能需要更新dist[k]以及path[k]。循环扫描图中顶点,对每个顶点进行以下检测:
如果set[vk]=1,说明vk已经加入路径,则什么都不用做;
如果set[vk]=0,说明vk还没有加入路径,则比较旧路径v0-vk的长度
dist[k]
和新路径v0-vu-vk的长度dist[u] + g.edges[u][k]
的大小,看是否新路径的长度更短,即是否满足dist[u] + g.edges[u][k] < dist[k]
。如果满足,则说明vu的加入使得v0到vk的路径长度更短,所以更新dist[k]为新路径的长度,同时更新path[k]=u。每加入一个新顶点,更新一次dist[k]以及path[k]。重复1,2步骤n-1次,完成从v0到其余各顶点的最短路径。路径保存在path[]数组中,且最短路径长度值保存在dist[]数组中。
辅助数组的初始值举例,v0为顶点0,dist[],path[],set[]三个辅助数组的初始值如下:
对上图的算法的执行过程模拟:
- 迪杰斯特拉算法
//迪杰斯特拉算法,求v0到其余各顶点的最短路径
void Dijkstra(MGraph g, int v0, int dist[], int path[]) {int set[maxSize]; //辅助标记数组int min, u; //两个辅助变量//初始化dist[],path[],set[]三个辅助数组for (int i = 0;i < g.vexnum;i++) { //辅助数组赋初始值dist[i] = g.edges[v0][i]; //顶点i与v0单边相连,则dist[i]为边的权值,不相连为INF;set[i] = 0; //所有顶点的标记值初始化为0,表示未加入路径//path[a]=b表示在路径中,顶点a前一个顶点是bif (g.edges[v0][i] < INF) { //如果vi与v0相连,则path[vi]=v0path[i] = v0; }else { //如果vi与v0不相连,则path[vi]=-1path[i] = -1;}}set[v0] = 1; //v0加入路径path[v0] = -1; //v0是路径的起点,规定path[v0]为-1//初始化结束,算法开始for (int i = 0; i < g.vexnum-1; i++){ //1.在剩余顶点中,从dist[]中选出最小值,表示通过已有路径走到这个顶点的路径是最短的min = INF;for (int j = 0; j < g.vexnum; j++) {if (set[j] == 0 && dist[j] < min) {u = j;min = dist[j];}}set[u] = 1; //新加入顶点为Vu//2.如果新顶点的加入使得从v0到vk的路径长度变得更短,则更新dist[k]以及path[k]for (int k = 0; k < g.vexnum; k++){ if (set[k] == 0 && dist[u] + g.edges[u][k] < dist[k]) {dist[k] = dist[u] + g.edges[u][k];path[k] = u;}}}
}
算法结束后,最短路径保存在path[]数组中,且最短路径长度值保存在dist[]数组中。path[]数组如下:
path[] 数组中其实保存了一棵树,这是一棵用双亲存储结构存储的树,通过这棵树可以打印出从源点到任何一个顶点最短路径上所经过的所有顶点。树的双亲表示法只能直接输出由叶子结点到根结点路径上的结点,而不能逆向输出,因此需要借助一个栈来实现逆向输出。
- 打印路径函数如下:
//a是目标顶点,输出从v0到a的最短路径
void printPath(int path[], int a) {int stack[maxSize], top = -1; //初始化栈while (path[a] != -1) { //从a往上到根结点依次入栈stack[++top] = a;a = path[a];}stack[++top] = a; //根结点入栈while (top != -1) {cout << stack[top--] << " ";}cout << endl;
}
- 算法的时间复杂度:O(n2)
- 当图中含有负权值时,迪杰斯特拉算法并不适用。
弗洛伊德(Floyd)算法
弗洛伊德(Floyd)算法主要用来求图中任意一对顶点vi和vj的最短路径。
- 基本思想
弗洛伊德算法的基本思想是递推n阶方阵,该算法需要用到两个矩阵:A和Path
- 矩阵A表示任意两顶点之间的路径长度。比如Ak[i][j]就表示顶点i到j的路径长度,而k表示以k顶点作为中间顶点的运算步骤。
- 矩阵Path表示用来记录当前两顶点最短路径上要经过的中间顶点。
逐步尝试在原路径中加入顶点k(k=0,1,2,3…n-1)作为中间顶点,若增加中间顶点后,得到的路径比原来的路径长度减少了,则用新路径代替原路径,直到所有顶点加入路径。
- 举例
对于下图最右边的图来说,写出它的邻接矩阵,得到初始矩阵A-1,同时矩阵Path-1中元素的初始值为-1,表示没有任意顶点对的路径之间都没有中间顶点:
对于每次新加入的顶点k,对于任⼀顶点对 (i, j),i ≠ j,k ≠ i,v ≠ j,如果A[i][j] > A[i][k] + A[k][j],则将 A[i][j] 更新为 A[i][k] + A[k][j] 的值,并且将 Path[i][j] 改为k。
- 加入顶点0(k=0),矩阵A和Path不变:
- 加入顶点1(k=1),检查所有顶点对(因为i ≠ j,k ≠ i,v ≠ j,实际上不用检查所有顶点对。首先,主对角线上的顶点对始终都是0,不需要更新,其次,第k行以及第k列上的顶点对都不需要检查),所以需要检查的顶点对就是0-2,0-3,2-0,2-1,2-3,3-0,3-1,3-2,发现A[0][2]>A[0][1]+A[1][2]=5+4=9,则将A[0][2]修改为9,同时将Path[0][2]改为1,表示此时0到2中的中间路径是1:
加入顶点2(k=2),需要检查的顶点对为0-1,0-3,1-0,1-1,1-3,3-0,3-1,3-3。按照上面的方法,得到A2和Path2:
加入顶点3(k=3),得到A3和Path3:
至此,所有的顶点都已经加入到路径中,所以A3和Path3就是最终的矩阵。
通过A矩阵可以找到图中任意两个结点之间的最短路径长度,比如A[1][3]为2,就表示顶点1到顶点3的最短路径长度为2。
通过Path矩阵可以写出任意两个结点的最短路径。比如从1到0,Path[1][0]为3,就表示3是中间顶点:
- 先写出1到3的路径,发现Path[1][3]为-1,表示顶点1到顶点3之间没有中间顶点,则1->3
- 再写3到0的路径,发现Path[3][0]为2,表示顶点3到顶点0之间有中间顶点2
- 写3到2的路径,发现Path[3][2]为-1,则3->2
- 再写2到0的路径,发现Path[2][0]=-1,则2->0
由此可以得到顶点1到顶点0的最短路径就是1-3-2-0。可以发现这是一个递归的过程,用代码表示:
void printPath(int i, int j, int path[][maxSize], int A[][maxSize]) {if (A[i][j] < INF) {if (path[i][j] == -1) {//直接输出边<i,j>;}else {int mid = path[i][j]; //中间顶点printPath(i, mid, path, A); //前半段路径printPath(mid, j, path, A); //后半段路径}}
}
- 弗洛伊德算法的代码表示
void Floyd(MGraph* g, int Path[][maxSize], int A[][maxSize]) {//初始化数组for (int i = 0; i < g->vexnum; i++)for (int j = 0; j < g->vexnum; j++){A[i][j] = g->edges[i][j];Path[i][j] = -1;}//算法的主体部分是一个三重循环,表示将k顶点逐步加入到路径中,并修改A矩阵和Path矩阵for (int k = 0; k < g->vexnum; k++)for (int i = 0; i < g->vexnum; i++)for (int j = 0; j < g->vexnum; j++)if (A[i][j] > A[i][k] + A[k][j]){A[i][j] = A[i][k] + A[k][j];Path[i][j] = k;}
}
- 算法复杂度:O(n3)
- 弗洛伊德算法允许图中带有负权值的边存在,但不允许包含带负权值的边组成的回路。
- 弗洛伊德算法同样适用于带权无向图。
- 也可以使用单源最短路径算法来解决任意顶点之间的最短路径问题,需要对每个顶点都使用一次Dijkstra算法,时间复杂度是O(n3)。
拓扑排序
DAG图:即有向无环图。
AOV网:即顶点表示活动的网络(Activity On Vertex network),用DAG图表示一个工程,其顶点表示活动,用有向边<Vi,Vj>表示活动之间的先后关系。
拓扑排序:是对DAG图顶点的一种排序,它使得若存在路径从顶点A到顶点B,则在拓扑排序中顶点B必然出现在A的后面。
拓扑排序满足下面的条件:
- 每个顶点出现且只出现一次;
- A在B的前面,则图中不存在从B到A的路径。
拓扑排序不唯一。
拓扑排序的方法:
- 从AOV网中选出一个没有前驱(入度为0)的顶点输出;
- 删除1中输出的顶点以及和它有关的边;
- 重复1和2,直到AOV网为空或者不存在没有前驱(入度为0)的顶点。
举例:
拓扑排序1:abced
拓扑排序2:abecd
拓扑排序3:aebcd
拓扑排序算法
需要用到一个入度数组indegree[]来记录顶点的入度:
int indegree[maxSize]; //入度数组,记录所有顶点的入度int TopSort(AGraph *g) {int n; //计数器 int stack[maxSize], top = -1; //初始化栈(不一定是栈,其他容器,比如队列也行,这里并没有对顺序要求)for (int i = 0; i < g->vexnum; i++) //将所有入度为0的顶点入栈{if (indegree[i] == 0){stack[++top] = i;}}//关键步骤while (top!=-1){int v = stack[top--]; //栈顶顶点出栈++n; //计数器+1cout << v << " "; //输出出栈顶点//将出栈顶点v指向的所有顶点的入度-1,并将入度减为0的顶点入栈ArcNode *p = g->adjlist[v].firstArc; while (p!=NULL){int k = p->adjvex;indegree[k]--;if (indegree[k] == 0){stack[++top] = k;}p = p->nextArc;}}if (n == g->vexnum) return 1;else return 0; }
算法时间复杂度:由于输出每个顶点的同时还要删除以它为起点的边,所以算法复杂度为O(n+e)
逆拓扑排序
对一个AOV网如下操作进行排序,称之为逆拓扑序列:
- 在网中任选一个没有后继的顶点(出度为0)输出
- 在网中删除和它有关的边(所有指向它的边)
- 重复1和2,直到AOV网为空或者不存在没有后继的顶点
- 使用DFS(深度优先搜索遍历)进行拓扑排序
当有向图无环时,可以用DFS(深度优先搜索遍历)先得到逆拓扑序列,进而得到拓扑序列。
由于图中无环,当由图中某个顶点出发进行DFS时,最先退出算法的顶点即为出度为0的顶点,就是逆拓扑序列的第一个顶点。所以,按照DFS算法的先后次序记录下的顶点序列即为逆向拓扑序列,将其逆向输出即为拓扑序列。
关键路径
- AOE网:在带权有向图中,用顶点表示时间,有向边表示活动,权值表示活动进行的时间,则称之为用边表示活动的网络(Activity On Edge network)。
- AOE网和AOV网都是有向无环图,区别在于它们的顶点和边表示的含义不同。
- AOE网的性质
- 只有在某个顶点所表示的事件发生后,从该顶点出发的各有向边代表的活动才能开始;
- 只有在所有指向某个顶点的边代表的所有活动都结束后,该顶点代表的事件才会发生;
- AOE网中只有一个入度为0的顶点,称为源点,代表着整个工程的开始;AOE网中只有一个出度为0的顶点,称为汇点,代表整个工程的结束。
- 关键路径:从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径。
- 关键活动:关键路径上的活动。
- 关键路径的长度代表完成整个工程所需要的时间,任何一个关键活动不能按时完成,整个工程的完成时间都会延后。所以,只有找到了关键路径,才能得到工程最短完成时间。
- 通过加快某些关键活动可以缩短整个工程的工期,但这也可能会导致该关键活动变成非关键活动。
- 关键路径并不唯一,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,必须加快那些包括所有关键路径上的关键活动才能缩短工期。
按照下面的步骤可以求得某活动的关键路径与关键活动:
事件vk的最早发生时间ve(k)
从源点v1到vk的最长路径长度。vk的最早发生时间决定了所有从顶点k开始的活动能够开工的最早时间。
推算方法:对图进行拓扑排序,按照拓扑排序的顺序求ve:
ve(源点)=0;
ve(k) = Max{ ve(j) + Weight(vj,vk) }
其中,j是k的任意前驱,Weight(vj,vk) 表示边<vj,vk>上的权值。也就是说,在图中可能有多条边指向k(k在图中能有多个直接前驱),而经过直接前驱j到k的这条路径最长,即ve(j)+ Weight(vj,vk) 最大,则取这个最大值作为ve(k)。
事件vk的最迟发生时间vl(k)
在不推迟整个工程完成的前提下,该事件最迟发生的时间。
推算方法:对图进行逆拓扑排序,按照逆拓扑排序的顺序求vl:
vl(汇点)=ve(汇点);
vl(k) = min{ vl(j) - Weight(vk,vj) }
假设j是k的后继事件(j可能有多个),vl(j)是事件j的最迟发生时间,从事件k到事件j的活动须花费时长Weight(vk,vj),所以vl(j) - Weight(vk,vj)表示相对于事件j来说事件k的最晚的发生时间。j可能有多个,k应该相对尽早地发生,才能满足所有的j在各自的最迟发生时间能发生,所以要取vl(j) - Weight(vk,vj)最小值,即为k的最迟发生时间。
活动ai的最早开始时间e(i)
即该活动的起点事件所发生的最早发生时间。若<vk,vj>表示活动ai,则e(i)=ve(k)。
活动ai的最迟开始时间l(i)
即该活动的终点事件的最迟发生时间与该活动所需要的时间之差。若<vk,vj>表示活动ai,则l(i)=vl(j)-Weight(vk,vj)。
活动的剩余时间:l(i)-e(i)
表示在不延长总工程完成时间的前提下,活动ai可以拖延的时间。当某个活动的剩余时间为0时,表示它必须如期完成,否则就会导致整个工期的延后,称这种活动为关键活动。所有关键活动构成的路径就是关键路径。
举例:
先求拓扑排序和逆拓扑排序:1346和6431。
按照拓扑排序,从源点处开始,求事件vk的最早发生时间ve(k)
按逆拓扑排序,从汇点处开始,求事件vk的最迟发生时间vl(k)。
规定,源点处的事件的最早发生时间是0,而汇点处的事件的最迟发生时间等于它的最早发生时间。
求事件的最早发生时间时,如果有多条边指向它,要选“ve(j) + Weight(vj,vk) ”最长的。
比如,求ve(6)时,ve(3)+a7=2+6 < ve(4)+a6=1+8=9,选ve(4)+a7
求事件的最迟发生时间时,如果它指向多条边,要选“vl(j) - Weight(vk,vj) ”最短的。
求vl(1)时,vl(4)-a3=1-1=0 < vl(3)-a2=3-2=1,选k vl(4)-a3
活动的最早开始时间就是引出这项活动的事件的最早发生时间,比如e(a2)=ve(1),e(a6)=ve(3)
活动的最迟开始时间就是活动的终点事件的最晚发生时间与活动持续时间的差。
关键活动是指:活动的最早开始时间=活动的最迟开始时间的活动。这里就是a3和a7。
关键路径:关键活动组成的路径。它的路径最长,它的持续时间代表了整个工程的最短完成时间。
例题
顶点个数为n的无向图最多有n(n-1)/2条边。(Cn2)
含有n个顶点的连通无向图最少有n-1条边。
含有n个顶点的连通有向图最少有n条边。
含有n个顶点的完全有向图含有n(n-1)条边。
使用下列(AB)方法可以判断出一个有向图是否有环?
A. 深度优先遍历
B. 拓扑排序
C. 求最短路径
D. 求关键路径
在有向图的DFS算法中,如果在遍历没有结束前,出现从孩子结点到父结点的边,自然说明这个有向图有环。
在拓扑排序中,每次要删除一个没有前驱的结点,如果到最后还有结点
当各边权值均相等时,BFS算法可以解决单源最短路径问题。
拓扑排序不唯一。
在拓扑排序中,如果A出现在B前面,则在图中必不会出现从B到A的路径。
无向图的邻接矩阵是对称矩阵。
对于无向图的邻接矩阵,顶点i的出度是指第i行元素之和,第j列的元素之和表示顶点j的入度。(1表示邻接,0表示不邻接)
对于有向图的邻接矩阵,顶点i的出度是指第i行所有非∞非0元素的个数,顶点i的入度是指第i列所有非∞非0元素的个数。
DFS算法类似于二叉树的先序遍历算法;BFS算法类似于二叉树的层次遍历算法。
这项活动的事件的最早发生时间,比如e(a2)=ve(1),e(a6)=ve(3)
活动的最迟开始时间就是活动的终点事件的最晚发生时间与活动持续时间的差。
关键活动是指:活动的最早开始时间=活动的最迟开始时间的活动。这里就是a3和a7。
关键路径:关键活动组成的路径。它的路径最长,它的持续时间代表了整个工程的最短完成时间。
例题
顶点个数为n的无向图最多有n(n-1)/2条边。(Cn2)
含有n个顶点的连通无向图最少有n-1条边。
含有n个顶点的连通有向图最少有n条边。
含有n个顶点的完全有向图含有n(n-1)条边。
使用下列(AB)方法可以判断出一个有向图是否有环?
A. 深度优先遍历
B. 拓扑排序
C. 求最短路径
D. 求关键路径
在有向图的DFS算法中,如果在遍历没有结束前,出现从孩子结点到父结点的边,自然说明这个有向图有环。
在拓扑排序中,每次要删除一个没有前驱的结点,如果到最后还有结点
当各边权值均相等时,BFS算法可以解决单源最短路径问题。
拓扑排序不唯一。
在拓扑排序中,如果A出现在B前面,则在图中必不会出现从B到A的路径。
无向图的邻接矩阵是对称矩阵。
对于无向图的邻接矩阵,顶点i的出度是指第i行元素之和,第j列的元素之和表示顶点j的入度。(1表示邻接,0表示不邻接)
对于有向图的邻接矩阵,顶点i的出度是指第i行所有非∞非0元素的个数,顶点i的入度是指第i列所有非∞非0元素的个数。
DFS算法类似于二叉树的先序遍历算法;BFS算法类似于二叉树的层次遍历算法。
数据结构学习笔记(6)——图相关推荐
- 【图神经网络】图神经网络(GNN)学习笔记:图分类
图神经网络GNN学习笔记:图分类 1. 基于全局池化的图分类 2. 基于层次化池化的图分类 2.1 基于图坍缩的池化机制 1 图坍缩 2 DIFFPOOL 3. EigenPooling 2.2 基于 ...
- 考研数据结构学习笔记1
考研数据结构学习笔记1 一.绪论 1.基本概念和术语 2.数据结构三要素 2.1逻辑结构 2.1.1 集合结构 2.1.2 线性结构:一对一 2.1.3 树形结构:一对多 2.1.4 图状结构:多对多 ...
- 考研[*数据结构*]学习笔记汇总(全)
文章目录: 一:预备阶段 二:基础阶段笔记 三:冲刺阶段笔记 四:各章节思维导图 五:题库 来源:王道计算机考研 数据结构 一:预备阶段 之前的数据结构笔记 数据结构--学习笔记--入门必看[建议收藏 ...
- 谷粒商城商品规格数据结构学习笔记(SPUSKU)
谷粒商城商品规格数据结构学习笔记(SPU&SKU) SPU Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集 SKU Stock Keeping Uni ...
- 数据结构学习笔记(王道)
数据结构学习笔记(王道) PS:本文章部分内容参考自王道考研数据结构笔记 文章目录 数据结构学习笔记(王道) 一.绪论 1.1. 数据结构 1.2. 算法 1.2.1. 算法的基本概念 1.2.2. ...
- 数据结构学习笔记(3-5):树
附录:所有blog的链接 数据结构学习笔记(1):基本概念 数据结构学习笔记(2):线性结构 数据结构学习笔记(3-5):树 数据结构学习笔记(6-8):图 数据结构学习笔记(9-10):排序 数据结 ...
- 数据结构学习笔记(七):哈希表(Hash Table)
目录 1 哈希表的含义与结构特点 1.1 哈希(Hash)即无序 1.2 从数组看哈希表的结构特点 2 哈希函数(Hash Function)与哈希冲突(Hash Collision) 2.1 哈希函 ...
- 数据结构学习笔记(六):二叉树(Binary Tree)
目录 1 背景知识:树(Tree) 2 何为二叉树(Binray Tree) 2.1 二叉树的概念与结构 2.2 满二叉树与完全二叉树 2.3 二叉树的三种遍历方式 3 二叉树及其遍历的简单实现(Ja ...
- 数据结构学习笔记(五):重识字符串(String)
目录 1 字符串与数组的关系 1.1 字符串与数组的联系 1.2 字符串与数组的区别 2 实现字符串的链式存储(Java) 3 子串查找的简单实现 1 字符串与数组的关系 1.1 字符串与数组的联系 ...
- 数据结构学习笔记(四):重识数组(Array)
目录 1 数组通过索引访问元素的原理 1.1 内存空间的连续性 1.2 数据类型的同一性 2 数组与链表增删查操作特性的对比 2.1 数组与链表的共性与差异 2.2 数组与链表增删查特性差异的原理 3 ...
最新文章
- 最后一片蓝海的终极狂欢-写在Win10发布前夕
- Android MVP模式 简单易懂的介绍方式
- Andoroid之BottomNavigationView右上角添加红点布局和自动跳转到底部具体第几个Tab
- getcwd函数_PHP getcwd()函数与示例
- 在Hibernate的session中同时有两个相同id的同类型对象,修改失败
- 【Python3网络爬虫开发实战】1.2.4-GeckoDriver的安装
- java面向对象特性_java面向对象编程三大特性
- hscan扫描mysql代码_HScan 扫描器
- 【php毕业设计】基于php+mysql+apache的在线购物网站设计与实现(毕业论文+程序源码)——在线购物网站
- 分布式系统中的CAP理论
- word如何用制表符对齐公式
- oracle取较小数,oracle 取小数位数
- Spring Boot的Maven插件Spring Boot Maven plugin详解
- 【Little Demo】从简单的Tab标签到Tab图片切换
- (干货)备战2021年软考中级网络工程师-04知识产权与标准化
- kafka-13-windows中安装kafka
- Elasticsearch用java api 创建mapping
- IDEA如何执行maven命令
- OpenCV-Python 级联分类器训练 | 六十三
- vim打开文件跳转到上次编辑的位置
热门文章
- 20230120英语学习
- 互联网早报:华为高精度地图拟年内商用,正在一线城市采集数据
- 2、Shell 脚本入门
- 得一微YS9082HP主控256G固态硬盘量产成功,简易教程,附YS9082HP开卡量产工具
- 新思路计算机等级考试50套,新思路计算机一级选择题50套(含答案)解析.doc
- HLS直播降低延迟的方法
- 通达OA系统管理员手册(一)
- ECMAScript渡一教育JavaScript精英课堂笔记 姬成
- Spartan6系列之Spartan6系列之芯片时钟资源深入详解
- Matlab:实现分析由反射器支撑的等角螺旋天线的行为(附完整源码)