图的存储
邻接矩阵法
- 空间复杂度:O(|V|²)---只和顶点数相关,和实际的边数无关
- 适用于存储稠密图
- 无向图的邻接矩阵是对称矩阵,可以压缩存储
- 只要确定了顶点编号,图的邻接矩阵表示方式唯一
#define MaxVertexNum 100 //顶点数目的最大值
typedef struct{
char Vex[MaxVertexNum]; //顶点表,顶点中可以存更复杂的信息
//各个顶点在数组Vex中的存放位置下标和二维数组Edge是相互对应的
int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表;可以用bool型或枚举型变量表示边
int vexnum,arcnum; //图的当前顶点数和边数/弧数
}MGraph;
/*
O(|V|)
无向图:第i个结点的度 = 第i行(或第i列)的非零元素个数---O(n)
有向图:
第i个结点的出度 = 第i行的非零元素个数
第i个结点的入度 = 第i列的非零元素个数
第i个结点的度 = 第i行、第i列的非零元素之和
*/
//带权图(网)
#define MaxVertexNum 100 //顶点数目的最大值
#define INFINITY 最大的int值 //宏定义常量"无穷"
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct{
VertexType Vex[MaxVertexNum];//顶点
EdgeType Edge[MaxVertexNum][MaxVertexNum];//边的权
int vexnum,arcnum; //图的当前顶点数和弧数
}MGraph;
//0或无穷:两个顶点之间不存在边
邻接表法(顺序+链式)
- 存储稀疏图
- 计算度/出度/入度:计算有向图的度、入度不方便,其余很方便
- 找相邻的边:找到向图的入边不方便,其余很方便
//用邻接表存储的图
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
//"顶点"
typedef struct VNode{
VertexType data; //顶点信息
ArcNode *first; //第一条边/弧
}VNode,AdjList{MaxVertexNum}
//"边/弧"
typedef struct ArcNode{
int adjvex; //边/弧指向哪个结点
struct ArcNode *next; //指向下一条弧的指针
//InfoType info; //边权值
}ArcNode;
/*
无向图中边的数量是冗余的,边结点数量是实际边数的两倍,整体空间复杂度为 O(|V|+2|E|)
度:遍历和这个顶点相关的边链表,同时可以找到和这个顶点相邻的所有边
有向图中边结点数量和实际一致,空间复杂度为O(|V|+|E|)
出度:遍历链表,该结点的所有往外射出的边
入度:找到指向结点的弧,把所有结点的边链表遍历一遍,找到指到该结点的弧,时间复杂度很高
图的邻接表表示方式并不唯一
*/
十字链表(存储有向图)
- 空间复杂度:O(|V|+|E|)
- 可以解决邻接表中找入边不方便的问题
- 十字链表法只用于存储有向图
// 定义两个结构体--弧结点,顶点结点
#define MAX_VERTEX_NUM 20
typedef struct ArcBox{
int tailvex,headvex; //该弧的尾和头顶点的位置
struct ArcBox *hlink,*tlink;//分别为弧头相同和弧尾相同的弧的链域
InfoType *info; //该弧相关信息的指针
}ArcBox;
typedef struct VexNode{
VertexType data;
ArcBox *firstin,*firstout; //分别指向该顶点的第一条入弧和出弧
}VexNode;
typedef struct{
VerNode xlist[MAX_VERTEX_NUM]; //表头向量
int vexnum,arcnum; //有向图的当前顶点数和弧数
}OLGraph;
邻接多重表(存储无向图)
- 空间复杂度:O(|V|+|E|)---每条边值对应一份数据
- 删除边、删除结点等操作很方便
#define MAX_VERTEX_NUM 20
typedef enum{unvisited,visited}VisitIF;
typedef struct EBox{
VisitIF mark; //访问标记
int ivex,jvex; //该边依附的两个顶点的位置
struct EBox *ilink,*jlink; //分别指向依附这两个顶点的下一条边
InfoType *info; //该边信息指针
}EBox;
typedef struct VexBox{
VertexType data;
EBox *firstedge; //指向第一条依附该顶点的边
}VexBox;
typedef struct{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum,arcnum; //无向图的当前顶点数和边数
}AMLGraph;
基本操作
- 图的遍历
/*
广度优先遍历BFS
无论树或图的广度优先遍历,重要的是找到该结点/顶点的下一个
树:不存在"回路"
图:搜索相邻的顶点时,有可能搜到已经访问过的顶点,加个变量来表示已经搜寻过的
1、找到与某一个顶点相邻的所有顶点
- FirstNeighbor{G,x};
- NextNeighbor{G,x,y};
2、标记哪些顶点被访问过
- bool visited[MAX_ERTEX_NUM];
3、需要一个辅助队列
*/
bool visited[MAX_ERTEX_NUM]; //访问标记数组,初始都为false
//BFS算法如果是非连通图,无法遍历完所有结点,则添加一条判断语句
void BFSTraverse(Graph,G){
for(i=0;i<G.vernum;i++)
visited[i] = FALSE; //访问标记数组初始化
InitQueue(Q); //初始化辅助队列
for(i=0;i<G.vernum;i++){ //从0号顶点开始遍历
if(!visited[i]) //对每个连通分量调用一次BFS
BFS(G,i); //vi未访问过,从vi开始BFS
}
}
void BFS(Graph G,int v){ //对图G进行广度优先遍历,从顶点v开始
visit(v); //访问初始顶点v
visited[v] = true;//对访问过的结点进行标记
EnQueue(Q,v); //访问后,顶点v入队
while(!isEmpty(Q)){//队列不空
DeQueue(Q,v);//将队头元素出队,对其进行后序操作
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)){
//开始循环遍历,w为v的第一个邻接顶点,操作结束后,该结点变为v(相对于w的)下一个邻接顶点,找到所有v的所有邻接点
if(!visited(w)){//如果该顶点没有被访问过
visit(w);//访问该结点
visited[w] = true;//该结点已被访问,标记
EnQueue(Q,w);//该结点入队
//
}
}
}
}
/*
--同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一
--同一个图邻接表表示方式不唯一,因此广度优先遍历序列不唯一
--空间复杂度:最坏情况-辅助队列大小为O(|V|)
--时间复杂度
邻接矩阵:访问|V|个顶点需要O(|V|)的时间,查找每个顶点的邻接点需要O(|V|)的时间,O(|V|²)
邻接表:访问O|V|)个顶点需要O(|V|)的时间,查个各个顶点的邻接点共需要O(|E|)的时间,O(|V|+|E|)
*/
/*
广度优先生成树:
根据广度优先遍历的过程得到的。
由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一
广度优先生成森林:
对非连通图的广度优先遍历,可得到广度优先生成森林
*/
/*
深度优先遍历--类似树的先根遍历
--空间复杂度主要来自于函数调用栈,最坏情况O(|V|),最好情况O(1)
--时间复杂度(与BFS相同)
邻接矩阵:访问|V|个顶点需要O(|V|)的时间,查找每个顶点的邻接点需要O(|V|)的时间,O(|V|²)
邻接表:访问O|V|)个顶点需要O(|V|)的时间,查个各个顶点的邻接点共需要O(|E|)的时间,O(|V|+|E|)
--同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一,深度优先生成树也唯一
--同一个图邻接表表示方式不唯一,因此深度优先遍历序列不唯一,深度优先生成树不唯一
*/
void DFSTrverse(Graph G){
for(v=0;v<G.vernum;++v)
visited[i] = FALSE;
InitQueue(Q);
for(v=0;v<G.vernum;++v){ //本代码中从v = 0开始遍历
if(!visited[v])
DFS(G,v);
}
}
bool visited[MAX_ERTEX_NUM]; //访问标记数组,初始都为false
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);
}
}
/*
深度优先生成树
深度优先生成森林
*/
图的连通性:
对无向图
1、进行VFS/DFS遍历调用BFS/DFS函数的次数 = 连通分量数
2、对于连通图,只需要调用1次BFS/DFS
对有向图
1、进行BFS/DFS遍历调用BFS/DFS函数的次数要具体问题具体分析
2、若起始顶点到其他各顶点都有路径,则只需要调用1一次BFS/DFS函数
//FirstNeighbor(G,x)
NexrNeighbor(G,x,y)
最小生成树(最小代价树)
连通图的生成树是包含图中全部顶点的一个极小连通子图(边尽可能的少,但要保持连通)
对于一个带权连通无向图G=(V,E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(MST)
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的
- 最小生成树的边数 = 顶点数-1,砍掉一颗则不连通,增加一条边则会出现回路
- 如果一个连通图本身就是一颗树,则其最小生成树就是它本身
- 只有连通图才有生成树,非连通图只有生成森林
/*
Prim算法(普里姆):
从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
时间复杂度:O(|V|²)适合用于边稠密图
isJoin[]:标记各结点是否已加入树
lowCast[]:各结点加入树的最低代价
第1轮:循环遍历所有个结点,找到lowCast最低的,且还没有加入树的结点
再次循环遍历,更新还没加入的各个顶点的lowCast值
第2轮:循环遍历所有结点,,找到lowCast最低的,且还没有加入树的结点
......
Kruskal算法(克鲁斯卡尔):
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选),直到所有结点都连通
时间复杂度:O(|E|log₂|E|)适合用于边稀疏图
初始:将各条边按权值排序
第1轮:检查第1条边的两个顶点是否连通(是否属于同一个集合)
不连通,则连起来
第2轮:检查第2条边的两个顶点是否连通
......
第n轮:检查第n条边的两个顶点是否连通
连通,则跳过
共执行e轮,每轮判断两个定点是否属于同一集合
*/
最短路径
/*
单源最短路径
BFS算法(无权图)
Dijkstra算法(带权图、无权图)
每对顶点间的最短路径
Floyd算法(带权图、无权图)
*/
BFS求无权图的单源最短路径
/*
无权图可以视为一种特殊的带权图,只是每条边的权值都为1
增加两个数组
d[]用来记录各个顶点到原始顶点的路径长度
path[]用来记录每个顶点在这个最短路径上的直接前驱
*/
//求顶点u到其他顶点的最短路径
void BFS_MIN_Distance(Graph G,int u){
d[i]表示从u到i结点的最短路径
for(i<0;i<G.vexnum;++i){
d[i] = ∞ //初始化路径长度
path[i] = -1;//最短路径从哪个顶点过来
}
d[u] = 0;
visited[u] = TRUE;
EnQueue(Q,u);
while(!isEmpty(Q)){ //BFS算法主过程
DeQueue(Q,u);
for(w=FirstNeighbor(G,u);w>=0;w=NextNeighbor(G,u,w)){
if(!visited[w]){ //w为u的尚未访问的邻接顶点
d[w] = d[u] + 1; //路径长度加1
path[w] = u; //最短路径应从u到w
visited[w] = TRUE;//设已访问标记
EnQueue(Q,w);//顶点w入队
}
}
}
}
Dijkstra算法(迪杰斯特拉)
/*
初始:从V₀开始,初始化三个数组信息
final[]:标记各顶点是否已找到最短路径
dist[]:最短路径长度
path[]:路径上的前驱
第1轮:循环遍历所有结点,找到还没确定最短路径,且dist最小的顶点Vi,令final[i] = true
"final"为false
检查所有邻接Vi的顶点,若其final值为false,则更新dist和path信息
第2轮:同第一轮
*/
/*
算法实现过程
初始:
若从V0开始,令final[0] = true;dist[0] = 0;path[0] = 0.
其余顶点final[k] = false;dist[k]=arcs[0][k];path[k]=(arcs[0][k]==∞)?-1:0
n-1轮处理:
循环遍历所有顶点,找到还没确定最短路径,且dist最小的顶点Vi,令final[i] = true。
并检查所有邻接自Vi的顶点,对于邻接自Vi的顶点Vj,
若final[j]==false && dist[i]+arcs[i][j]<dist[j],
则令dist[j]=dist[i]+arcs[i][j]
arcs[i][j]表示Vi到Vj的弧的权值,若是没有弧,则设为无穷
时间复杂度:O(n²)即O(|V|²)
*/
void ShortestPath_DIJ(MGraph G,int 0,PathMatrix &P,ShortPathTable &D){
//用Dijsktara算法求有向网G的V0顶点到其余顶点V的最短路径P[v]及其带权长度D[v]
//若P[v][w]为TRUE,则w是从V0到V当前求得最短路径上的顶点
//final[v]为TRUE当且仅当v∈S,即已经求得从V0到V的最短路径
for(v=0;v<G.vexnum;++v){
final[v] = FALSE;
D[v] = G.arcs[v0][v];
for(w=0;w<G.vexnum;++w)
P[v][w] = FASLE; //设空路径
if(D[v]<INFINITY){
P[v][v0] = TRUE;
P[v][v] = TRUE;
}
}
D[v0] = 0;
final[v0] = TRUE; //初始化,v0属于S集
//开始主循环,每次求得V0到某个V顶点的最短路径,并加V到集
for(i=1;i<G.vexnum;++i){ //其余G.vexnum -1各顶点
min = INFINITY; //大当前所知离V0顶点的最近距离
for(w=0;w<G.vexnum;++w)
if(!final[w]) //w顶点在V-S中
if(D[w]<min){ //w顶点离v0顶点更近
v = w;
min = D[w];
}
final[v] = TRUE; //离v0顶点最近的v加入S集
for(w=0;w<G.vexnum;++w) //更新当前最短路径及距离
if(!final[w] && (min+G.arcs[v][w]<D[w])){ //修改D[w]和P[w],w∈V-S
D[w] = min + G.arcs[v][w];
P[w] = P[v];
P[w][w] = TRUE;
}
}
}
//如果有负权值的话,可能失效
Floyd算法(弗洛伊德)
/*
求出每一对顶点之间的最短路径
使用动态规划思想,将问题的求解分为多个阶段
对于n个顶点的图G,求任意一堆顶点Vi-->Vj之间的最短路径可分为以下几个阶段:
初始:不允许在其他顶点中转,最短路径是?
#0:若允许在V0中转,最乱路径是?
#1:若允许在V0,V1中转,最短路径是?
....
#n-1:若允许在V0,V1,V2...V(N-1)中转,最短路径是?
两个数组:
A:目前来看,各顶点间的最短路径长度
path:两个顶点之间的中转点
*/
void ShortestPath_FLOYD(MGraph G,PathMatrix &P[],DistancMatrix &D[]){
for(v=0;v<G.vexnum;++v){
for(w=0;w<G.vexnum;++w){
D[v][w] = G.arcs[v][w];
for(u=0;u<G.vexnum;++u)
P[v][w][u] = FALSE;
if(D[v][w]<INFINITY){
P[v][w][v] = TRUE;
P[v][w][w] = TRUE;
}
}
}
for(u=0;u<G.vexnum;++u){
for(v=0;v<G.vexnum;++v){
for(w=0;w<G.vexnum;++w){
if(D[v][u] + D[u][w] <D[v][w]){
D[v][w] = D[v][u] + D[u][w];
for(i=0;i<G.vexnum;++i)
P[v][w][i] = P[v][u][i] || P[u][w][i];
}
}
}
}
}
/*
//......准备工作,根据图的信息初始化矩阵A和path
for(int k=0;k<n;k++){ //考虑以Vk作为中转点
for(int i=0;i<n;i++){ //遍历整个矩阵,i为行号,j为列号
for(int j=0;j<n;j++){
if(A[i][j]>A[i][k] + A[k][j]){ //以Vk为中转点的路径更短
A[i][j] = A[i][k] + A[k][j];//更新最短路径长度
path[i][j] = k; //中转点
}
}
}
}
//时间复杂度:O(|V|³) 空间复杂度O(|V|²)
//可以决解带有负权值的图,但不能解决带有"负权回路"的图(有负权值的边组成回路),这种图有可能无最短路径
*/
有向无环图
- 若一个有向图中不存在环--DAG图
- 顶点中不可能出现重复的操作数
/*
Step1:把各个操作数不重复地排成一排
Step2:标出各个运算符的生成顺序(先后顺序有点出入无所谓)
Step3:按顺序加入运算符,注意"分层"
Step4:从低向上逐层检查同层的运算符是否可以合体
*/
AOV网:用顶点表示活动的网
用DAG图表示一个工程。顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
- 每个顶点出现且只出现一次
- 若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径
- "找到做事的先后顺序"
/*
拓扑排序的实现
- 从AOV网中选择一个没有前驱(入度为0)的顶点并输出
- 从网中删除该顶点和所以它为起点的有向边
- 重复1和2直到当前的AOV网为空或当前网中不存在无前驱的顶点为止
每个AOC网都有一个或多个拓扑排序序列
当前所有顶点入度>0,说明原图存在回路
*/
//indegerr[];当前顶点入度
//print[];记录拓扑序列
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode{ //边表结点
int adjvex; //该弧指向的顶点的位置
struct ArcNode *nextrac; //指向下一条弧的指针
//InfoType info; //网的边权值
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct{
AdjList vertices; //邻接表
int vexnum,arcnum; //图的顶点数和弧数
}Graph;//Graph是以邻接表存储的图类型
bool TopologicalSort(Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
for(int i=0;i<G.vexnum;i++){
if(indegree[i] = 0)
Push(S,i); //将所有入度为0的顶点入栈
}
int count = 0; //计数,记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不空,则存在入度为0的顶点
Pop(S,i); //栈顶元素出栈
print[count++] = i;//输出顶点i
for(p=G.vertices[i].firstarc;p=p->nextarc){
//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈S
v = p->adjvex;
if(!(--indegree[v]))
Push(S,v); //入度为0,则入栈
}
}
if(count<G.vexnum)
return false; //排序失败,有向图中有回路
else
return true; //拓扑排序成功
}
//时间复杂度:O(|V|+|E|)
//若采用邻接矩阵,则需要O(|E|²)
对于一个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序:
- 从AOV网中选择一个没有后继(出度为0)的顶点并输出
- 从网中删除该顶点和所有以它为终点的有向边
- 重复1和2直到当前的AOV网为空
//在DFS基础上,得到逆拓扑排序序列
void DFSTraverse(Graph G){ //对图G进行深度优先遍历
for(v=0;v<G.vexnum;++v)
visited[v] = TRUE;
for(v=0;v<G.vexnum;++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实现逆拓扑排序:在顶点退栈前输出
print(v);//输出顶点
}
//思考:如果存在回路,则不存在逆拓扑排序序列,如何判断回路?
关键路径
AOE网:在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络。
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
- 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生
- 另外,有些活动是可以并行的进行的
- 在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始
- 也仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束
- 从源点到汇点的有向路径可能有多条,在所有路径中:
- 具有最大路径长度的路径称为关键路径
- 关键路径上的活动称为关键活动
- 完成整个工程的最短时间就是关键路径的长度
- 若关键活动不能按时完成,则整个工程的完成时间就会延长
- 缩短关键活动的时间,可以缩短整个工程的工期
- 当缩短到一定程度时,关键活动可能会变成非关键活动
- 可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路上的关键活动才能达到缩短工期的目的
/*
事件Vk发生的最找时间ve(k)---决定了所有从vk开始的活动能够开工的最早时间
活动ai的最早开始时间e(i)----指该活动弧的起点所表示的事件的最早发生时间
事件vk的最迟发生时间vl(k)---它是指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间
活动ai的最迟开始时间l(i)----它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差
求关键路径的步骤:
-- 1、求所有事件的最早发生时间ve()
按拓扑排序序列,依次求各个顶点的ve(k):
ve(源点) = 0
ve(k) = Max{ve(j) + Weight(vj,vk)},vj为vk的任意前驱
-- 2、求所有事件的最迟发生事件vl()
按逆拓扑排序序列,依次求各个顶点的v(k)
vl(汇点) = ve(汇点)
vl(k) = Min{vl(j) - Weight(vk + vj)},vj为vk的任意后继
-- 3、求所有活动的最早发生时间e()
若边<vk,vj>表示活动ai,则有e(i) = ve(k)
-- 4、求所有活动的最迟发生时间l()
若边<vk,vj>表示活动ai,则有l(i) = vl(j) - Weight(vk,vj)
-- 5、求所有活动的时间余量d() d(i)=0的活动就是关键活动,由关键活动可得关键路径
d(i) = l(i) - e(i)
*/