文章目录
- **一 图的基本概念**
- **1 定义**
- **二 图的存储及基本操作**
- **1 邻接矩阵法**
- **2 邻接表法**
- **3 十字链表**
- **4 邻接多重表**
- **5 图的基本操作**
- **三 图的遍历**
- **1 广度优先搜索BFS**
- **2 深度优先搜索DFS**
- **3 图的遍历与连通性**
- **四 图的应用**
- **1 最小生成树**
- **==1.1 Prim(普里姆)算法==**
- **==1.2 Kruskal(克鲁斯卡尔)算法==**
- **2 最短路径**
- **==2.1 Dijkstra算法求单源最短路径问题==**
- **==2.2 Floyd算法求各顶点直接最短路径问题==**
- **2.3 有向无环图描述表达式**
- **==2.4 拓扑排序==**
- **==2.5 关键路径==**
一 图的基本概念
1 定义
图G由顶点集V和边集E组成,记为G=(V,E);|V|表示顶点个数,|E|表示边的条数
线性表可以是空表,树可以是空树,但是图不能是空图
(1)有向图
从顶点1到顶点2,1称为弧尾,2称为弧头
(2)无向图
没有方向的边
(3)简单图,多重图
简单图:不存在重复的边,不存在顶点到自身的边,如上面两图
多重图:某两个顶点直接的边数大于1条,又允许顶点通过一条边和自身相连
(4)完全图(简单完全图)
对于无向图,有n*(n-1)/2条边的无向图称为完全图,其任意两个顶点之间都存在边
对于有向图,有==n(n-1)==条弧的有向图称为有向完全图,其任意两个顶点之间都存在方向相反的两条弧
(5)子图
如有两个图G,G`,V’是V的子集,E‘是E的子集,则G’是G的子图
若V(G’)=V(G)的子图G’,则称其为G的生成子图
但不是任意子集都能构成子图,有些可能甚至不是图
(6)连通,连通图和连通分量
连通:无向图中,顶点v到顶点w有路径存在,则v和w连通的
连通图:图G中任意两个顶点都是连通的,否则称为非连通图
无向图中的极大连通子图称为连通分量
如果一个图有n个顶点,边数小于n-1,则必是非连通图;同理,如果图是非联通图,那么最多有n-2条边
(7)强连通图,强连通分量
强连通:有向图中,一对顶点v和w,从v到w和从w到v都有路径,称他们为强连通
强连通图:图中任何一对顶点都是强连通的
有向图中的极大强连通子图称为有向图的强连通分量
一个n个顶点的强连通图最少有n+1条边
(8)生成树,生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图,若有n个顶点,则含有n-1条边
去掉一个边就是非连通图,加一个边就形成回路
非连通图中,连通分量的生成树构成了非连通图的生成森林
极大连通子图:无向图的连通分量,要求该连通子图包含所有的边
极小连通子图:既要保持图连通,又要使得边数最少的子图
(9)顶点的度,入度和出度
无向图中,顶点v的度是指依附于v的边的条数,记为TD(v),每个顶点都要算一次,n条边就2n的度
有向图中,入度是以顶点v为终点的有向边的数目,记为ID(v),出度是以顶点v为起点的有向边的树,记为OD(v),度就是两者之和,且整个图的ID=OD=n(图的边数)
(10)边的权和网
每条边标上具有某种含义的数值称为该边的权值,这种带有权值的图称为带权图也称网
(11)稠密图,稀疏图
边数多就是稠密,少就是稀疏,是相对概念
(12)路径,路径长度和回路
顶点v到w经过的顶点序列就是路径,路径上的边就是路径长度,第一个顶点和最后一个顶点相同的路径成为回路或环
n个顶点,大于n-1条边的图一定有环
(13)简单路径,简单回路
简单路径:路径序列中顶点不重复出现
简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路
(14)距离
距离:顶点v到w的最短路径长度,不存在就标记为无穷
(15)有向树
一个顶点的入度为0,其余顶点的入度均为1的有向图
二 图的存储及基本操作
1 邻接矩阵法
指用一个一维数组存储图顶点信息,用一个二维数组存储图中边的信息
邻接矩阵:存储顶点直接邻接关系的二维数组
#define maxvertexnum 100 \\顶点数目
typedef char vertextype; \\顶点的数据类型
typedef int edgetype; \\带权图中边上权值的数据类型
typedef struct
{vertextype Vex[maxvertexnum]; \\顶点表edgetype Edge[maxvertexnum][maxvertexnum]; \\邻接矩阵,边表int vexunm,arcnum; \\当前顶点数和弧数
}A[i][j]=1或0 表示从顶点i到j存在边就是1,否则是0
对带权图,A[i][j]=w 表示从i到j的权值是w,如不存在边就是0或者无穷
无向图的邻接矩阵是对称矩阵,并且唯一,可采用压缩存储;第i行非零元素的个数正好是顶点i的度
对有向图的第i行非零是i的出度,第i列非零是入度
邻接矩阵表示法的空间复杂度为O(n2),n是顶点数
要确定边数需要按行和列进行检测,时间慢,适合稠密图
邻接矩阵A,An的元素An[i][j]等于从i到j的长度为n的路径数目
2 邻接表法
对图的每个顶点vi建立一个单链表,第i个单链表中的结点表示依附于vi的边(对于有向图就是以vI为尾的弧),称为边表(对有向图就是出边表)
边表的头指针和顶点的数据信息采用顺序存储(顶点表),所以有顶点表结点和边表结点
#define maxvertexnum 100 \\顶点数目typedef struct Arcnode \\边表结点
{int adjvex; \\该弧指向顶点的位置struct Arcnode *next; \\指向下一条弧的位置\\Infotype info; \\边权值
}Arcnode;typedef struct Vnode \\顶点表结点
{Vertextype data; \\顶点信息Arcnode *first; \\指向第一条依附该顶点的弧的指针
}Vnode,Adjlist[maxvertexnum];typedef struct
{Adjlist vertices; \\邻接表int vexnunm,arcnum; \\图的顶点数和弧数
}Algraph;
(1)无向图的存储空间为O(|v|+|2e|),有向图存储空间为O(|V|+|E|)
(2)适合稀疏图,极大的节省空间
(3)若要确定两个顶点间是否存在边,邻接矩阵可以直接查到,邻接表需要在相应的结点对应的边表中查找另一结点,效率较低
(4)有向图邻接表,顶点的出度只需看邻接表的结点个数,但是入度要查整个表
(5)链表次序不唯一
3 十字链表
十字链表中,对应有向图中的每条弧有一个结点,对应每个顶点也有一个结点
tailvex:指示弧尾顶点的编号
headvex:指示弧头顶点的编号
hlink:指向弧头相同的下一个弧结点
tlink:指向弧尾相同的下一个弧结点
info:存放弧的相关信息
弧头相同的就在同一个链表,弧尾相同的在同一个链表
data:顶点的数据信息
firstin:指向以该顶点为弧头的第一个弧结点
firstout:指向以该结点为弧尾的第一个弧结点
顶点结点之间顺序存储,很容易找到头和尾,即出入度
4 邻接多重表
无向图的另一种链式存储,每条边都用一个结点表示
ivex和jvex指示该边依附的两个顶点的编号;ilink指向下一条依附于顶点ivex的边,jlink指向下一条依附于顶点jvex的边;info存放边的相关信息
data:顶点信息
firstedge:指向第一条依附于该顶点的边
所有依附于同一顶点的边串联在同一链表中,每个边结点同时链接在两个链表中
5 图的基本操作
仅抽象考虑
Adjacent(G,x,y); \\判断G是否存在边<x,y>
Neighbors(G,x); \\列出图G中与结点x邻接的边
Insertvertex(G,x);\\插入顶点x
Deletevertex(G,x);\\删除x
Addedge(G,x,y); \\若边<x,y>不存在,则添加
Removeedge(G,x,y);\\若边<x,y>存在,则删除
Firstneighbor(G,x);\\求顶点x的第一个邻接点,有返回顶点号,没有或不存在x返回-1
Nextneighbor(G,x,y);\\y是x的一个邻接点,返回除y的下一个邻接点,没有返回-1
Get_edge_value(G,x,y);\\获取权值
Set_dege_value(G,x,y,v);\\设置边的权值为v
三 图的遍历
1 广度优先搜索BFS
基本思想:访问起始结点v,从v出发,依次访问v的各个未访问过的邻接结点w1….wn,然后再依次访问w1….wn的所有未被访问过的邻接结点,然后再循环从结点出发,访问未被访问过的邻接结点,直到全部访问完毕
若还有没被访问的,重新选一个起始结点重复上述过程,直到全部访问完毕
dijkstra算法和prim最小生成树算法应用了这种思想
是一种分层的查找过程,类似树的层序遍历,需要借助队列,伪代码如下:
bool visited[maxvertexnum]; \\标记数组
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); \\每个连通分量一次BFS,没被访问过就BFS}}
}void BFS(Graph G,int v) \\从v出发,遍历G
{ visit(v); \\访问vvisited[v]=true; \\置为true防止多次访问Enqueue(Q,v); \\v入队列while(!isempty(Q)) {Dequeue(Q,v); \\v出队列for(w=Firstneighbor(G,v);w>=0; w=Nextneighbor(G,v,w)) \\检测v的所有邻接接点{if(!vitited[w]) \\w为v未访问过的邻接接点{visit(w); visited[w]=true;Enqueue(Q,w); \\访问w后,w再入队列}}}
}
图的广度优先搜索和二叉树的层序遍历基本一致
BFS的算法性能分析
空间复杂度为O(|V|)
时间复杂度:(1)邻接表:O(|V|+|E|)
(2)邻接矩阵:O(|V|2)
BFS算法求解单源最短路径的问题
非带权图的最短路径d(u,v):从u到v的任何路径中最少的边数,没有则置为无穷
void BFSmindistance(Graph G,int u)
{\\d[i]表示u到i的最短路径for(int i =0;i<G.vextun;i++){d[i]=∞;}visited[u]=true;d[u]=0;Enqueue(Q,u);while(!isempty(Q)){Dequeue(Q,u); \\队头元素u出队for(w =Firstneighbor(G,u); w>=0; w=Nextnrighbor(G,u,w)){if(!visited[w]) \\w为u未访问的邻接结点{visited[w]=true;d[w]=d[u]+1; \\路径长度加1Enqueue(Q,w);}}}
}
广度优先生成树
遍历过程中可以得到一颗遍历树
邻接矩阵的树唯一,但是邻接表的不唯一
2 深度优先搜索DFS
类似树的先序遍历,尽可能“深”的搜索一个树
基本思想:访问起始顶点v,从v出发,访问与c邻接且未被访问的任意一个顶点w1,再访问与w1邻接且未被访问的任意一个顶点w2,重复这个过程,不能向下访问的时候,依次退回最近被访问的顶点, 若它还有未被访问的邻接顶点,则再从这个顶点开始搜索,直到图访问完毕
递归算法:
bool visited[maxvertexnum];
void DFStraverse(Graph G)
{for(int v=0 ;v<G.vextum;v++){visited[v]=false;}for(int v = 0;v<G.vextum;v++){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=Nextneighbor(G,v,w)){if(!visited[w]){DFS(G,w);}}
}
DFS遍历的序列,邻接矩阵唯一,邻接表不唯一
DFS算法的性能分析
需要一个递归工作栈,空间复杂度为O(|V|)
时间复杂度:(1)邻接矩阵:O(|v|2)
(2)邻接表:O(|V|+|E|)
深度优先的生成树和生成森林
连通图调用DFS才能产生深度优先生成树,否则产生的是深度优先生成森林
3 图的遍历与连通性
无向图调用DFS,BFS的次数等于图的连通分量数
连通有向图分为强连通个非强连通,连通子图也分为强连通分量和非强连通分量
非强联分量一次调用BFS(G,i)或DFS无法访问到该连通分量的所有顶点
四 图的应用
1 最小生成树
一个带权连通无向图G,生成树不同,每棵树的权也可能不同,若T为权值之和最小的那颗生成树,则T为G的最小生成树MST
性质:
(1)最小生成树不唯一
(2)最小生成树的边的权值之和唯一
(3)最小生成树的边数为顶点数减1
根据性质从最小权值的边开始,基于贪心算法的有Prim和Kruskal算法
1.1 Prim(普里姆)算法
基本思想:任取一个顶点,找权值最小边并且不是重复顶点的边加入,直到所有顶点加入
n个顶点,T必有n-1条边
void Prim(G,T)
{T=空; \\初始化空树U={w}; \\添加任意一个顶点wwhile((V-U)!=空){设(u,v)是使得u属于U与v属于(V-U),且权值最小的边;T=T∪{(u,v)}; \\边归入树U=U∪{v}; \\顶点归入树}
}
时间复杂度:O(|V|2)
1.2 Kruskal(克鲁斯卡尔)算法
是一种按权值的递增次序选择合适的边来构造最小生成树的方法
void Kruskal(G,T)
{T=V; \\初始化T,仅含顶点numS=n; \\连通分量数,初始就是顶点总数while(numS>1){从E中取权值最小的边(v,u)if((v,u)属于不同连通分量){T=T∪{(u,v)}; \\边归入树numS--; }}
}
时间复杂度:O(log2|E|)
2 最短路径
一个顶点到另一个顶点的路径的权值之和最短称为最短路径;两点直接的最短路径也包含了路径上其他顶点间的最短路径
单源最短路径:求图中某一顶点到其他各顶点的最短路径,可通过Dijkstra算法
每对顶点间的最短路径:Floyd算法求解
2.1 Dijkstra算法求单源最短路径问题
设置一个集合S记录已求得的最短路径的顶点,初始时把源点v0放入S,集合S每并入一个新顶点vi,都要修改源点v0到集合V-S中顶点当前的最短路径长度值。
设置两个辅助数组:
dist[]:记录源点v0到其他各顶点当前的最短路径长度,初态为:若从v0到vi有弧,则dist[i]为弧上的权值,否则置为无穷
path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点
初始S只包含顶点0;邻接矩阵arcs表示带权有向图,arcs[i] [j]表示边<i,j>的权值,不存在就置为无穷
算法步骤:
(1)初始化:S初始为{0},dist[i]=arcs[0] [i],i=1,2,3,……n-1
(2)从顶点集合V-S中选出vj,满足dist[j]=Min{dist[i]|vi ∈ \in ∈V-S},vj就是当前求得的一条从v0出发的最短路径的终点,S=S ⋃ \bigcup ⋃{j}
(3) 修改从v0出发到集合V-S上任意一个顶点Vk可达的最短路径长度,若dist[j]+arcs[j] [k]<dist[k],则更新dist[k]=dist[j]+arcs[j] [k]
(4)重复2-3的操作n-1次,直到所有顶点都包含在S中,基于贪心策略
举例:
(1)初始化S,v1可达v2和v5,不可达v3和v4;故dist[2]=0,dist[3]=∞,dist[4]=∞;dist[5]=5
(2)选出最小值dist[5],将顶点v5并入集合S,此时找到1-5的最短路径;5加入后,1到集合V-S中可达顶点的最短路径长度可能会产生变化,更新dist[]数组,5可达2,1-5-2的距离8比dist[2]=10小,更新dist[2]=8;5可达3,1-5-3的距离14,更新dist[3]=14;5可达4,1-5-4的距离是7,更新dist[4]=7
(3)再选出最小值,dist[4],顶点4并入S,再更新dist[]数组;4不可达2,不变;4可达3,1-5-4-3距离是13比dist[3]小,更新dist[3]=13
(4)再选出最小的dist[2],顶点2并入S;更新dist[];2可达3,1-5-2-3的距离是9,比dist[3]小,更新dist[3]=9
(5)选出唯一的最小值dist[3],并入S,此时全部顶点并入完毕
无论是邻接矩阵还是邻接表的时间复杂度都是O(|V|2)
但是当边有负权值时,Dijkstra算法失效
例如图a,0到1和2的距离分别是7和5,那么就会首先并入顶点2,距离为5,再并入顶点1,但是并入顶点1时,从0到2的距离最小实际是2,不是5,但由于已经并入了2,无法更新
2.2 Floyd算法求各顶点直接最短路径问题
求出任意两个顶点i和j之间的最短路径和最短路径长度
基本思想:递推产生一个n阶方阵序列A(-1),A(0),A(1)……A(n-1),其中A(k)[i][j]表示从顶点i到顶点j的路径长度,k表示绕行第k个顶点的运算步骤。
初始时,任意两个顶点i和j直接存在边,则在边上的权值作为它们之间的最短路径长度;不存在有向边就用∞作为长度。
然后逐步尝试在原路径中加入顶点k作为中间顶点,若增加中间顶点后,得到的路径比原来的路径长度减少了,新路径就代替原路径
算法描述:
定义一个n阶方阵序列A(-1),A(0),A(1)……A(n-1),其中A(k)[i][j]=arcs[i] [j]
A(k)[i][j]=Min{A(k-1)[i][j],A(k-1)[i][k]+A(k-1)[k][j]},k=0,1……n-1
A(0)[i][j]是从顶点i到j,中间顶点是0的最短路径的长度;A(k)[i][j]是从顶点i到j,中间顶点的序号不大于k的最短路径的长度
这是一个迭代的过程,每迭代一次多考虑一个顶点;A(n-1)[i][j]就保存了任意一对顶点之间的最短路径长度
举例:
(1)初始化:方阵A(-1)[i][j]=arcs[i] [j]
(2)把0作为中间顶点,对所有顶点有A(-1)[i][j]>A(-1)[i][0]+A(-1)[0][j],把A(-1)[i][j]更新为A(-1)[i][0]+A(-1)[0][j];更新后方阵记为A0
(3)把1作为中间顶点,继续检测所有顶点,例如有A(0)[0][2]>A(0)[0][1]+A(0)[1][2]=10,更新,新方阵记为A1
(4)把2作为中间顶点,继续,更新方阵为A2,A2就是保存的任意顶点对的最短路径长度
时间复杂度:O(|V|3),但代码紧凑,对中等规模的输入来说仍然有效
允许图中有带负权值的边,但不允许与包含带负权值的边组成的回路;也适用于带权无向图
也可以用单源最短路径算法解决每对顶点之间的最短路径问题,轮流将每个顶点作为源点,在所有边权值均非负时,运行一次Dijkstra算法,时间复杂度为O(|V|3)
2.3 有向无环图描述表达式
若一个有向图不存在环,就成为有向无环图,简称为DAG图
例如((a+b)*(b*(c+d)+(c+d)*e))*((c+d)*e)
2.4 拓扑排序
AOV网:若用DAG图表示一个工程,其顶点表示工地,边表示活动i必须先于活动j进行的一种关系,则将这种有向图称为顶点表示活动的网络,边没有权值
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足以下条件时称为一个图的拓扑排序:
(1)每个顶点只出现一次
(2)若顶点A在序列中排在顶点B的前面,则在图中不存在从B到A的路径
拓扑排序是对有向无环图的的顶点的一种排序,使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面,每个AOV网都有一个或多个拓扑排序序列
常见算法步骤:
(1)从AOV网中厕一个没有前驱的顶点输出
(2)删除该顶点和它为起点的有向边
(3)重复1,2直到没有顶点为止
bool Topologicalsort{Graph G}
{Initstack(S);int i ;for(i=0;i<G.vextum;i++){if(indegree[i]==0) \\所有入度为0的顶点入栈{Push(S,i);}}int count =0; \\记录当前输出的顶点数while(!isempty(S)){Pop(S,i);print[count++]=i; \\输出顶点ifor(p=G.vertices[i].firstarc; p ; p=p->nextarc) \\所有i执行的顶点的入度减1,并把入度为0的入栈{v=p->adjvex;if(!(--indegree[v])){Push(S,v); }}}if(count<G.vexnum)return false; \\有环路 失败elsereturn true;
}
邻接表:O(|V|+|E|)
邻接矩阵:O(|V|2)
逆拓扑排序:
(1)选一个出度为0 的顶点输出
(2)删除顶点和它以它为终点的有向边
(3)重复1,2直到AOV网空
2.5 关键路径
AOE网:顶点表示事件,有向边表示活动,边的权值表示完成活动的开销,是有向无环图
性质:(1)只有某顶点的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
(2)进入某顶点的各有向边的活动都结束时,该顶点代表的事件才发生
(3)仅有一个入度为0的顶点,称为开始顶点(源点);仅有一个出度为0的顶点,称为结束顶点(汇点)
(4)有些活动可以并行进行
(3)源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,关键路径的长度就是完成工程的最短时间
寻找关键活动的参量:
(1)事件vk的最早发生时间ve(k)
(2)事件vk的最迟发生时间vl(k)
(3)活动ai的最早开始时间e(i)
(4)活动ai的最迟开始时间l(i)
(5)一个活动ai的最迟开始时间l(i)和最早开始时间e(i)的差额d(i)=l(i)-e(i)
l(i)-e(i)=0,即活动aI是关键活动
算法步骤:
(1)从源点出发,ve(源点)=0,按拓扑有序求其余顶点的最早发生时间ve()
(2)从汇点出发,令vl(汇点)=ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间vl()
(3)根据各顶点的ve()值求所有弧的最早开始时间e()
(4)根据各顶点的vl()值求所有弧的最迟开始时间l()
(5)求AOE网中的所有活动的差额d,找出所有d=0的活动构成关键路径
怎么求ve,ve就是从源点到这个点的耗时最长的数值
怎么求vl,从汇点倒着来,用汇点的值减去汇点到这个点的最小的值(尽量小的到达这个),所以vl ⩾ \geqslant ⩾ve
e就是弧起点的顶点的ve
l(i)等于该弧的终点的顶点的vl()减去弧的持续时间
关键路径上的所有活动都是关键活动
网中的关键路径并不唯一,只有加快所有关键路径上的关键活动才能缩短工期