图(多对多关系)
定义及名词解释
- 图:顶点的有穷非空集合和边的集合,常用G表示一个图,V表示顶点集合,E是边的集合,其中图至少有一个顶点,且顶点数量为有限个,边的集合可以为空
- 有向图和无向图:图按有无方向可以分为无向图和有向图,无向图称顶点和边,有向图中称顶点和弧,箭头指向的一方为弧头
- 稀疏图和稠密图:图按边和弧的多少分稀疏图和稠密图
- 完全图和简单图:任意两个顶点间都存在边叫完全图,无重复边或顶点到自身的边叫简单图
- 度、出度、入度:图中顶点有邻接点、依附的概念,无向图顶点边数叫度,有向图顶点分出度和入度
- 网:当图的边或弧带有权值,则称其为网
- 环/回路和简单路径:图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复的叫简单路径
- 连通图:若任意两顶点都是连通的,则图就是连通图,有向图称为强连通图
- 连通分量:图中有子图,若子图达到极大连通则就是连通分量,有向图中称为强连通分量
- 生成森林和有向树:无向图中连通且n个顶点n-1条边称为生成树,有向图中一顶点入度为0其余顶点入度为1的叫有向树,一个有向图由若干棵有向树构成生成森林
存储结构
邻接矩阵
可表示为一个二维数组,如图所示
当图为无向图时,默认为双向,即整个矩阵沿对角线对称
当图为有向图时,由a指向b的边将在矩阵中以array【a】【b】=1的方式表示
当图不带权值时,1表示两点连通,0表示不连通
当图带权值时,连通时矩阵中的值为两点间的权值,点和它自己标为0,不连通的两点标为无穷
邻接表
以一维数组存储顶点值,并各自作为头结点,连接其邻接元素的链表,如图所示
其中链表结点的顺序是不固定的,不一定要按顺序排列,而链表部分中,结点的adjvex区域存储的是头结点元素的邻接元素的下标,而不是具体元素的值
十字链表
十字链表是有向图的链式存储,结合了有向图的邻接表和逆邻接表
一维数组存储了顶点的值,并作为后续链表的头结点,出弧指针域指向邻接元素(如上图的v0的出弧指针域指向存有弧尾0,弧头1的结点),而入弧指针域指向有弧指向头结点的元素(如上图中的v0的入弧指针域指向存有弧尾2,弧头0的结点)
在非头结点的结点内部,存有弧头和弧尾元素的下标,以及指向弧头相同的其他结点和弧尾相同的其他结点的指针
邻接多重表
邻接多重表是针对无向图和无向网的,可看作是邻接表和十字链表的结合
以一维数组存储了顶点的值,并作为后续链表的头结点,非头结点的结点中包括用于标记是否被操作过的标志域(mark),存储图中边两端的顶点下标的数据域(ivex和jvex,顺序不分),以及指向同一个ivex和同一个jvex的其他结点的指针域(ilink和jlink,顺序不分),除此之外,有时还多设一个用于存储边的权值的数据域
由于邻接多重表的空间利用率过低,最好还是使用邻接表和邻接矩阵
图的遍历
深度优先遍历
深度优先遍历类似于树的先序遍历,大致思路以下图为例
- 任意取一个未被遍历过的顶点(如v1),标记为已访问
- 遍历v1的邻接点(如v2),并标记为已访问,逐步向下,遍历v2的邻接点,即v4,然后访问v8,再访问v5,都标记为已访问
- 当遍历v5的邻接点时,v2和v8都已经被标记为已访问,故而回退到v8,同理回退到v4,v2,最后到v1
- 在v1处找到未访问的顶点v3,故而停止回退,继续遍历v3的邻接点v6,然后v7
- 遍历v7的邻接点都已经被访问,于是回退到v6,v3,最后回到v1
- 判断是否所有顶点都被访问,如果还有未被访问的,继续按照以上方法访问,否则结束遍历
深度优先遍历总的来说是个不断回溯的过程
#include <stdio.h> #define MAX_VERtEX_NUM 20 #define VRType int #define InfoType char #define VertexType int typedef enum{false,true}bool; bool visited[MAX_VERtEX_NUM]; typedef struct { VRType adj; InfoType * info; }ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM]; typedef struct { VertexType vexs[MAX_VERtEX_NUM]; AdjMatrix arcs; int vexnum,arcnum; }MGraph;
int LocateVex(MGraph * G,VertexType v){ int i=0; for (; i<G->vexnum; i++) { if (G->vexs[i]==v) { break; } } if (i>G->vexnum) { printf("no such vertex.\n"); return -1; } return i; }
void CreateDN(MGraph *G){ scanf("%d,%d",&(G->vexnum),&(G->arcnum)); for (int i=0; i<G->vexnum; i++) { scanf("%d",&(G->vexs[i])); } for (int i=0; i<G->vexnum; i++) { for (int j=0; j<G->vexnum; j++) { G->arcs[i][j].adj=0; G->arcs[i][j].info=NULL; } } for (int i=0; i<G->arcnum; i++) { int v1,v2; scanf("%d,%d",&v1,&v2); int n=LocateVex(G, v1); int m=LocateVex(G, v2); if (m==-1 ||n==-1) { printf("no this vertex\n"); return; } G->arcs[n][m].adj=1; G->arcs[m][n].adj=1; } } int FirstAdjVex(MGraph G,int v) { for(int i = 0; i<G.vexnum; i++){ if( G.arcs[v][i].adj ){ return i; } } return -1; } int NextAdjVex(MGraph G,int v,int w) { for(int i = w+1; i<G.vexnum; i++){ if(G.arcs[v][i].adj){ return i; } } return -1; } void visitVex(MGraph G, int v){ printf("%d ",G.vexs[v]); } void DFS(MGraph G,int v){ visited[v] = true; visitVex( G, v); for(int w = FirstAdjVex(G,v); w>=0; w = NextAdjVex(G,v,w)){ if(!visited[w]){ DFS(G,w); } } }
void DFSTraverse(MGraph G){ int v; for( v = 0; v < G.vexnum; ++v){ visited[v] = false; } for( v = 0; v < G.vexnum; v++){ if(!visited[v]){ DFS( G, v); } } } int main() { MGraph G; CreateDN(&G); DFSTraverse(G); return 0; }
|
广度优先遍历
广度优先遍历类似于树的层次遍历,即从某一个顶点开始,遍历其所有邻接点,然后再顺序遍历这些邻接点的所有邻接点,直到所有邻接的顶点被访问后,判断图中是否还有未被访问的结点,若有则重复以上过程
广度优先遍历的实现借助了队列的结构,实现代码如下
#include <stdio.h> #include <stdlib.h> #define MAX_VERtEX_NUM 20 #define VRType int #define InfoType char #define VertexType int typedef enum{false,true}bool; bool visited[MAX_VERtEX_NUM]; typedef struct Queue{ VertexType data; struct Queue * next; }Queue; typedef struct { VRType adj; InfoType * info; }ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM]; typedef struct { VertexType vexs[MAX_VERtEX_NUM]; AdjMatrix arcs; int vexnum,arcnum; }MGraph;
int LocateVex(MGraph * G,VertexType v){ int i=0; for (; i<G->vexnum; i++) { if (G->vexs[i]==v) { break; } } if (i>G->vexnum) { printf("no such vertex.\n"); return -1; } return i; }
void CreateDN(MGraph *G){ scanf("%d,%d",&(G->vexnum),&(G->arcnum)); for (int i=0; i<G->vexnum; i++) { scanf("%d",&(G->vexs[i])); } for (int i=0; i<G->vexnum; i++) { for (int j=0; j<G->vexnum; j++) { G->arcs[i][j].adj=0; G->arcs[i][j].info=NULL; } } for (int i=0; i<G->arcnum; i++) { int v1,v2; scanf("%d,%d",&v1,&v2); int n=LocateVex(G, v1); int m=LocateVex(G, v2); if (m==-1 ||n==-1) { printf("no this vertex\n"); return; } G->arcs[n][m].adj=1; G->arcs[m][n].adj=1; } } int FirstAdjVex(MGraph G,int v) { for(int i = 0; i<G.vexnum; i++){ if( G.arcs[v][i].adj ){ return i; } } return -1; } int NextAdjVex(MGraph G,int v,int w) { for(int i = w+1; i<G.vexnum; i++){ if(G.arcs[v][i].adj){ return i; } } return -1; }
void visitVex(MGraph G, int v){ printf("%d ",G.vexs[v]); }
void InitQueue(Queue ** Q){ (*Q)=(Queue*)malloc(sizeof(Queue)); (*Q)->next=NULL; }
void EnQueue(Queue **Q,VertexType v){ Queue * element=(Queue*)malloc(sizeof(Queue)); element->data=v; Queue * temp=(*Q); while (temp->next!=NULL) { temp=temp->next; } temp->next=element; }
void DeQueue(Queue **Q,int *u){ (*u)=(*Q)->next->data; (*Q)->next=(*Q)->next->next; }
bool QueueEmpty(Queue *Q){ if (Q->next==NULL) { return true; } return false; }
void BFSTraverse(MGraph G){ int v; for( v = 0; v < G.vexnum; ++v){ visited[v] = false; } Queue * Q; InitQueue(&Q); for( v = 0; v < G.vexnum; v++){ if(!visited[v]){ visited[v]=true; visitVex(G, v); EnQueue(&Q, G.vexs[v]); while (!QueueEmpty(Q)) { int u; DeQueue(&Q, &u); u=LocateVex(&G, u); for (int w=FirstAdjVex(G, u); w>=0; w=NextAdjVex(G, u, w)) { if (!visited[w]) { visited[w]=true; visitVex(G, w); EnQueue(&Q, G.vexs[w]); } } } } } } int main() { MGraph G; CreateDN(&G); BFSTraverse(G); return 0; }
|
最小生成树
普里姆算法
普里姆算法(Prim算法)或称DJP算法,可在加权连通图里搜索最小生成树。由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小
克鲁斯卡尔算法
克鲁斯卡尔算法从边的角度求网的最小生成树,和普里姆算法恰恰相反,更适合于求边稀疏的网的最小生成树
对于任意一个连通网的最小生成树来说,在要求总的权值最小的情况下,最直接的想法就是将连通网中的所有边按照权值大小进行升序排序,从小到大依次选择
由于最小生成树本身是一棵生成树,所以需要时刻满足以下两点:
- 生成树中任意顶点之间有且仅有一条通路,也就是说,生成树中不能存在回路
- 对于具有 n 个顶点的连通网,其生成树中只能有 n-1 条边,这 n-1 条边连通着 n 个顶点
连接 n 个顶点在不产生回路的情况下,只需要 n-1 条边
所以克鲁斯卡尔算法的具体思路是:将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。筛选出来的边和所有的顶点构成此连通网的最小生成树。
判断是否会产生回路的方法为:在初始状态下给每个顶点赋予不同的标记,对于遍历过程的每条边,其都有两个顶点,判断这两个顶点的标记是否一致,如果一致,说明它们本身就处在一棵树中,如果继续连接就会产生回路;如果不一致,说明它们之间还没有任何关系,可以连接。
最短路径
迪杰斯特拉算法
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。
它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
操作步骤
- 初始时,S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离”[例如,U中顶点v的距离为(s,v)的长度,然后s和v不相邻,则v的距离为∞]
- 从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时,从U中移除顶点k
- 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离
- 重复步骤(2)和(3),直到遍历完所有顶点
弗洛伊德算法
Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似。该算法是一种在具有正或负边缘权重(但没有负环)的加权图中找到最短路径的算法,即支持负权值但不支持负权环。
相较而言,迪杰斯特拉计算的是单源最短路径,而弗洛伊德计算的是多源最短路径,其时间复杂度为O(n³)。虽然它不返回路径本身的细节,但是可以通过对算法的简单修改来重建路径,我们利用这个思想,通过递归的方式访问每条路径经过的中间节点,对最终的路径进行输出。
拓扑排序
对一个有向无环图进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。
在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列,由AOV网构造拓扑序列的过程叫做拓扑排序。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
由AOV网构造拓扑序列的拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止:
- 选择一个入度为0的顶点并输出
- 从网中删除此顶点及所有出边
循环结束后,若输出的顶点数小于网中的顶点数,则有回路,否则输出的顶点序列就是一种拓扑序列。
关键路径
在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径,通过优化关键路径能够实现有效的程序优化,同拓扑排序一样,关键路径也不是唯一的。
由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为始点到终点的最大路径长度。关键路径长度是整个工程所需的最短工期。
如图,由v1到v9即一条关键路径,其关键路径长度为29,当其他路线中边的权值相加相等时,图中也可能出现多条关键路径