目录
图
一.图的数据结构
二.图的存储结构
常用有邻接表、邻接多重表、十字链表
2.1(数组表示法)顺序存储——邻接矩阵
- 为n*n矩阵,分有向图与无向图。无向图邻接矩阵关于主对角线对称,操作时需对对称位置一起操作。(邻接1,否则0)
- 网的邻接矩阵中,邻接边赋权值,未邻接边赋值无穷(一般为自定义一个足够大的数)
- 度:出度及入度分开(有向)
#define INFINITY INT_MAX //即代表无穷的一个大数
#define MAX_VERTEX_NUM 20 //图的最大的顶点个数
typedef enum{DG,DN,UDG,UDN} GraphKind; //{有向图,有向网,无向图,无向网}
typedef struct ArcCell{
VRType adj; //VRType是顶点关系类型。对无权图,用1或0表示相邻否;对带权图,则为权值类型
InfoType * info;//该弧相关信息的指针
}ArcCell,AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //定义该结构体类型分别为ArcCell代表一个点类型,AdjMaxtrix代表一个有这么大规模的二维结构体组类型
typedef struct{
VertexType vex[MAX_VERTEX_NUM]; //顶点向量
AdjMatrix arcs; //邻接矩阵
int vexnum,arcnum; //图的当前定点数和弧数
GraphKind kind; //图的种类标志
}Mgraph;
则有图类型的各种变量:
MGraph G; //定义G为图
G.vexnum; //顶点个数
G.arcnum; //边的个数
G.kind; //图的类型
G.vex[i]; //顶点i的信息
G.arcs[i][j].adj; //顶点i和顶点j的关系
G.arcs[i][j].info->; //边的附加关系
图的操作FirstAdjVex()和NextAdjVex()的实现(邻接矩阵):
直接找邻接矩阵某行/列的不为零的点即可。
2.2邻接表
对每个顶点建立一个单链表,设置头结点为一维向量存储结点信息以及一个指针指向第一个邻接结点的表结点。无向图一个即可,有向图则需设置一个邻接表和一个逆邻接表
#define MAX_VERTEX_NUM 20
typedef struct ArcNode{ //表结点(弧)
int adjvex; //该弧所指向的顶点的位置
struct ArcNode *nextarc;//指向下一条弧的指针
InfoType *info; //该弧相关信息的指针
}ArcNode;//表结点
typedef struct VNode{
VertexType data; //顶点信息
ArcNode *firstarc;//指向第一条依附于该顶点的弧的指针
}VNode,AdjList[MAX_VERTEX_NUM];//定义VNode为该结构体类型的声明,AdjList为该结构体这么大规模的一维向量类型的声明。
typedef struct{
AdjList vertices;//顶点集向量
int vernum;
int arcnum;
int kind;
}ALGraph;
则与之对应的图类型的变量:
ALGraph G;//声明邻接表存储类型的图G
G.vernum; //顶点个数
G.arcnum; //弧的个数
G.vertices[i].data; //顶点i的信息
G.vertices[G.vertices[i].firstarc->adjvex].data; //结点i的第一个邻接结点的信息
G.vertices[i].firstarc->nextarc->adjvex;//顶点i的第二个邻接点的在一维表中的位置
2.3十字链表(有向图)
顶点集仍是一维向量顺序存储,每一个头结点中包含三个域:data节点信息、firstout第一个邻接结点的弧的指针,firstin第一个以该结点为尾的弧的指针(及将邻接表与逆邻接表结合)
弧结点则包含了五个域:tailvex弧尾结点的位置、headvex弧头结点的位置,hlink指针指向弧头相同的下一条弧,tlink指针指向弧尾相同的下一条弧,info指针域指向弧的相关信息。
#define MAX_VERTEX_NUM 20
typedef struct ArcBox{
int headvex,tailvex; //该弧的尾和头顶点的位置
struct ArcBox *hlink,*tlink; //分别为弧头相同和弧尾相同的弧的链域,即抽象出来的一个n*n二维表格(邻接表)中该行的下一个弧结点的指针和该列的下一个弧结点的指针。但又由于是头插法加的弧结点,不会以顺序的方式依次链接
InFoType info;//该弧相关信息的指针
}ArcBox;//弧结点
typedef struct VexNode(
VertexType data;
ArcBox *firstin,*firstout;//分别指向该顶点的第一条入弧和第一条出弧
}VexNode;
typedef struct{
VexNode xlist[MAX_VERTEX_NUM];//头结点向量
int vexnum,arcnum;//有向图当前顶点数和弧数
}OLGraph;//Orthogonal List十字链表
2.4 无向图的邻接多重表
是对邻接矩阵的压缩,可以抽象看作是邻接矩阵的下三角。
在边的操作上较为方便,每一个边结点同时连接在两个链表中。
边结点6个域:
mark:标志域,标记是否被搜索过
ivex和jvex:该边依附的两个顶点在图中的位置
ilink和jlink:链域,分别指向下一条依附于顶点ivex获jvex的边
info:边信息
头结点2个域:
data
firstedge指示第一条依附于该点的边(通常一行的第一个,若没有则一列的第一个)
逻辑上链表如上图,但由于多用头插法,故再说
#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;
}VerBox;
typedef struct{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum,edgenum;//无向图的当前顶点数和边数
}AMLGraph;
三.图的搜索算法
遍历图时应注意:
- 确定遍历的起点,为保证非连通图的每一个顶点都能被访问到,应迭代轮换起点
- 为了避免重复访问,应作访问标记
3.1 DFS深度优先搜索
Boolean visited[MAX]; //访问标志数组
Status (*VisitFunc)(int v); //函数变量
void DFSTraverse(Graph G,Status (*Visit)(int v)){
VisitFunc =Visit; //使用全局变量VisitFunc,使DFS不必设函数指针参数。即函数声明为声明一个全局变量的函数指针的一个变量
for(v=0;v<G.vexnum;++v) visited[v]=FALSE;
for(v=0;v<G.vexnum;++v)
if(!visited[v]) DFS(G,v); //对尚未访问的顶点调用DFS
}
void DFS(Graph G,int v)
{
visited[v]=TRUE;
for(w=FirstAdjVex(G,v);w>=0;w=NextAdjVex(G,v,w))
if(!visited[w]) DFS(G,w);
}
3.2 BFS广度优先搜索
void BFSTravers(Graph G,v)
{
for(v=0;v<G.vexnum;++v)
visited[v]=FALSE
InitQueue(Q);//初始化一个队列
for(v=0;v<G.vexnum;++v)
{
if(!visited[v])
{
EnQueue(Q,v)
while(!QueueEmpty(Q))
{
DeQueue(Q,u);
for(w=FirstAdjVex(G,u);w>=0;w=NextAdjVex(G,u,w))
{
if(!visited[w])
{
visited[w]=TRUE;
visit(w);
EnQueue(Q,w);
}//if
}//for
}//while
}//if
}
}
注:遍历图的过程实际为对每一个顶点查找其邻接点的过程,耗费时间取决于所采用的存储结构。当用二维数组表示邻接矩阵作图的存储结构时,为O(n2);采用邻接表作图的存储结构时,为O(n+e),(e为边的条数)
对应有先广编号的无向图和树边集T
3.3思考
- 图的路径问题
(1)无向图两点之间是否有路径存在?
递归判断邻接点是否为指定点或邻接点与指定点是否有路径存在,是则返1,否则返0
int ExistPathDfs1(ALGraph G,int *visited,int u,int v)//u、v间是否存在路径,邻接表实现
{
ArcNode *p;//弧结点指针
int w;
if(u==v)
return 1;
else
{
visited[u]=TRUE;
for(p=G.vertices[u].firstarc;p;p=p->nextarc)
{
w=p->adjvex;
if(!visited[w]&&ExistPathDfs1(G,visited,w,v))//注意需要判断该结点未被访问过
return 1;
}
}//else
return 0;
}
- 有向图两点之间是否有路径存在
类比无向图判断路径的代码
int ExistPathDfs2(ALGraph G,int *visited,int u,int v)
{
int w;
ArcNode *p;
if(u==v)
return 1;
else
{
visited[u]=TRUE;//对于传进来的参数结点即赋值“已被访问”
for(p=G.vertices[u].firstarc;p;p=p->nextarc)
{
w=p->adjvex;
if(!visited[w]&&ExistPathDfs2(G,visited,w,v))
return 1;
}//for
}//else
}//ExistPathDfs2
static静态局部变量
先回顾下static声明的变量的作用
int ExitPathDfs3(ALGraph G,int *visited,int u,int v)//静态局部变量做标记,只要在某个子进程中符合条件修改值,则在程序的整个生命周期中都会时被修改的状态(即使再声明也不会初始化了)
{
static int flag=0;
int w;
ArcNode *p;
visited[u]=TRUE;
p=G.vertices[u].firstarc;
while(p)
{
w=p->adjvex;
if(w==v)
{
flag=1;
return 1;
}
if(!visited[w])
{
ExitPathDfs3(G,visited,w,v);
}
p=p->nextarc;
}
if(!flag) return 0;
return 1;
}
(3)如果有路径,路径经过那些顶点?
直接在return 1时访问结点打印信息即可
(4)求u到v的所有简单路径
int FindAllPath(ALGraph G,int *visited,int *path,int u,int v,int k)//path为一维向量记录路径,path[0]为起点,从path[1]开始为路径中的顶点,若不存在路径则从path[1]起全是0
{
ArcNode *p;
int static paths;//记录第几条路径
int w,i;
path[k]=u;
if(u==v)
{
if(path[1])//确保路径存在
{
if(!paths) print("找到如下路径:\n");//当路径条数为0时打印该信息
paths++;
for(i=0;path[i];i++)
printf("%d\t",path[i]);
}
}
else
{
for(p=G.vertices[u].firstarc;p>=0;p=p->nextarc)
{
w=p->adjvex;
if(!visited[w])
{
FindAllPath(G,visited,path,w,v,k+1)
}
visited[u]=0;
path[k]=0;//进行到此说明这个点已找到其后的所有路径,无记录访问过的必要了
return paths;
}
}
}
- 图的环路问题
(1)无向图是否存在环路
(2)有向图是否存在环路
(3)有几条环路?
2*n的visit数组分别标记:是否作为起点过、在某次寻找路径时是否已经访问过,作为判断环路标志。
(4)环路经过哪些点,轨迹?
参找上一点,且加path数组,找到后输出path对应区间
四.图与树的联系
开放树:连通而无环路的无向图
4.1最小生成树
即代价最小
MST性质两个结点集之间最小代价边一定在最小生成树中
求最小生成树的算法
4.1.1 Prim算法
(运用MST性质)
引入辅助向量:
CloseST[]和LowCost:
CloseST[i]:U中一个顶点;
边(i,CloseST[i])具有最小的权LowCost[i]。
即对于点集V-U中的第i个结点,CloseST[i]为U中与它邻接的结点中权最小的那一个结点,最小权值为LowCost[i]。
==O(n^2^),与边无关而与点相关,适用于稠密网的最小生成树==
Costtype C[n+1][n+1];
void Prim(C)
{
costtype LowCost[n+1];
int CloseST[n+1];
int i,j,k;
costtype min;
for(i=2;i<=n;i++)
{
LowCost[i]=C[1][i];
CloseST[i]=1;
}//for赋初值初始化
for(i=2;i<=n;i++)
{
min=LowCost[i];
j=i;
for(k=2;k<=n;k++)
{
if(LowCost[k]<min)
{min=LowCost[k];j=k;}
}
cout<<j<<CloseST[j];<<endl;
LowCost[j]==INFINITY;
for(k=2;k<=n;k++)
if(j!+k&&C[j][k]<LowCost[k]&&LowCost[k]!=INFINITY)
{LowCost[k]=C[j][k];CloseST[k]=j;}//调整更新}
}
}//Prim
4.1.2 Kruskal算法
T中每个顶点自己作为一个连通分量,按照边的权不减的顺序,依次考查E中的每条边;若被考查的边连接不同分量中的两个顶点,则合并两个分量;否则放弃。当连通分量个数为1,说明生成了一个最小生成树
O(eloge)。利用堆存放网中的边O(e);对e-1条边进行”并“操作,每次”并“操作要根溯O(loge)故为O(eloge)
五.无向图的双连通性
关节点、回退边、双连通、重连通图(无关节点)……
求关节点:对图进行一次先深搜索便可求出所有关节点
定义:
(1)若生成树的根有两株或两株以上的子树,则次=此根节点必为关节点(第一类)
(2)若生成树中非叶顶点v,其某株子树的根和子树中的其它结点均没有指向v的祖先的回退边,则v是关节点(第二类关节点)
遍历过程,O(e+n)
求无向图双连通分量步骤
- 计算先深编号dfn[v]
- 计算low[v].其中,low[w]=min(dfn[v],dfn[w],low[y])。其中w是由v出发的回退边另一端,y是v的所有儿子。
- 求关节点:
(1)树根是关节点,当且仅当他有两个或两个以上的儿子
(2)非树根v是关节点当且仅当v有某个儿子y,使low[y]>=dfn[v]
5.1 求最短路
5.1.1 Floyed算法
求多源最短路问题,时间复杂度为O(n3),空间复杂度为O(n2).
核心思想是,从1-n顺序允许第i个个结点作为中转点,更新每两点间的最短路dst[k]。
define inf 999999
void Floyed(int edge[n+1][n+1])
{
int i,j,k;
for(i=1;i<=n;i++)//允许第i个结点做中转结点
{
for(j=1;j<=n;j++)//对以j为源到其它各点的最短路进行更新
{
for(k=1;k<=n;k++)//对从j到k的最短路进行更新
{
if(edge[j][i]<inf&&edge[i][k]<inf)//从j到k可以经过i进行中转
{
if(edge[j][k]>edge[j][i]+edge[i][k])
edge[j][k]=edge[j][i]+edge[i][k];//若经i中转的路径比不经i中转的路径段则赋中转值
}//if
}//for k
}//for j
}//for i
}
有向图的中心
拥有据其它各点最短路径的最大值的最小值的顶点为有向图的中心
5.2.2 dijkstra算法**
O(n2)
求单源最短路径。
思想:将点集V分为U和(V-U)。初始时U中仅有源点,假设为v1。
D[i]为v1到第i个点的当前的最小路径。
S[i]为第i个点是否在U中(即D[i]值为从v1到i的绝对最小路径)
对于U与(V-U),当后者不为空时,在(V-U)中选择D[w]为最小值,则D[w]即为绝对最小值。将w加入U中,然后更新(V-U)中的结点的D[i]:看经过点w中转的路径是否比原值小,是则更新。
void Dijkstr(Graph G,costtype D[MAXVEX+1])
{
int S[MAXVEX+1];
for(int i=1;i<=n;i++)//初始化
{
D[i]=G[1][i];
S[i]=0;
}
S[1]=1;
for(i=1;i<=n;i++)
{
w=mincost(G,S);//w为(U-V)中D[i]最小的点
S[w]=1;
for(int j=1;j<=n;j++)
{
if(S[j]==0&&D[j]>D[w]+G[w][j])
{
D[j]=D[w]+G[w][j];
}
}//for
}//for
}//Dijkstra
int mincost(Graph G,int S)
{
costtype min=INIFITY;
int temp;
for(int i=1;i<=n;i++)
{
if(S[i]==0&&D[i]<min)
{
min=D[i];
temp=i;
}
}
return i;
}
带路径的Dijkstra算法
思想:若从i到j为最短路,则从i到该路径中任一个结点k的路径为i到k的最短路。故设置P[i],为从1到i的最短路经中到第i个结点的上一个结点
void Dijkstra2(Graph G,costtype D[MAXVEX+1])
{
int S[MAXVEX+1],P[MAXVEX+1];
for(int i=1;i<=n;i++)
{
D[i]=G[i][i];
S[i]=0;
P[i]=1;
}
S[1]=1;
for(int i=1;i<=n;i++)
{
w=mincost(G,S);
S[w]=1;
for(int j=1;j<=n;j++)
{
if(S[j]==0&&D[j]>D[w]+G[w][j])
{
D[j]=D[w]+G[w][j];
P[j]=w;
}
}
}
Display(P,v);
}//Dihkstra2
void Display(int *P,int v)
{
if(P[v]!=1)
{
Display(P,P[v]);
printf("%d--",P[v]);
}
}//注意逆序输出的递归调用方法
六.有向图搜索
- 树边、向前边、回退边、横边
- 归约图&&分支横边
6.1 强连通
任意两点可互达
强连通分支
6.2求强连通分支
根据定义, 对同一张图同时进行正反两次遍历, 对两次的遍历结果取交集, 这里得到的便是强连通图。
参考——有向图强连通
概要:
-
映射思想: 算法求出的是原顶点到新顶点编号的一个映射,即数组scc[i]的含义为:原图中i顶点的强连通分量编号为scc[i]。
-
Kosaraju算法
性质:原图的强连通分量与反图的强连通分量一致
步骤:
a)对反图G’求一次后序遍历,按照遍历完毕的先后顺序将所有顶点记录在数组order中。
b)按照order数组的逆序,对原图G求一次先序遍历,标记连通分量。对反图上的两个点a和b,如果a能够到b,则a的时间戳大于b,b属于a的DFS树中的子孙结点。
那么如果在原图中,a也能够到b,则说明在反图中b能够到a,又由于原图和反图的强连通一致,所以a和b属于同一个强连通。由于第二次遍历是时间戳大的顶点开始遍历,遍历完标记,所以a能够到达的点的时间戳一定是小于a的时间戳的(大于a时间戳的顶点已经在逆序访问的时候先被标记掉了),令到达的点为b,则b在反图上和a的关系为a->b,这是利用了时间戳的相对大小来确定谁是谁的子孙结点。那么原图a->b,反图也是a->b,所以a和b属于同一个强连通,
————————————————
版权声明:本文为CSDN博主「英雄哪里出来」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/WhereIsHeroFrom/article/details/79417926
七.有向无环图的应用(DAG)
有向无环图及其应用
AOV-网(Active On Vertex Network):用顶点表示活动,用弧表示活动间的优先关系的有向图
7.1 拓扑排序
可输出结点入度为0,删去该结点,并将与该结点相邻的顶点入度减一。loop
直至全部顶点均已输出,或当前图中不存在入度为零的顶点为止。后者表明存在环。
算法实现:
邻接表的有向图存储结构,在头结点中增加一个存放顶点入度的数组(indegree)。为了避免重复检测入度为零的顶点,可另设一栈暂存所有入度为零的顶点。
7.1.1 应用栈
Status TopologicalSort(ALGraph G)
{
FindDegree(G,indegree); //对各顶点求入度indegree[1,VEXNUM]
int top=-1;//注:不能为0
for(int i=0;i<VEXNUM;i++)
{
for(int j=1;j<=VEXNUMLj++)
{
if(indegree[j]==0)//若入度为0,则该结点进栈,此时indegree[i]信息已赘余,可以利用作为栈结构信息的存储,即信息改为上一个栈顶元素的序位。
{
indegree[j]=top;
top=j; //进栈
}
}//for j
output(G,top);//打印输出栈顶元素
for(int p=G.vertices[top].firstarc;p;p=p->nextarc)//相连结点入度-1
{
int w=p->adjvex;
indegree[w]-=1;
}
top=indegree[top]; //已经输出过的结点因为indegree里面记录的是在栈中时的上一个元素的序位
}//fori
}
建立求各顶点入度的时间复杂度O(n),对每个边的操作是入度减一,为O(e)故时间复杂度为O(n+e)
应用队列
队不为空时,出栈一个顶点,检查其邻接点,如果度为零则入栈。
DFS后根顺序生成拓扑序列
void topodfs(v)
{
mark[v]=True;
for(v的每一个邻接点w)
{
if(visit[w]==FALSE)
topodfs(w);
}
output(v);//后根序。需要从结尾开始,在拓扑图的反图上进行。
}
7.2 关键路径
- 关键活动的速度的提高是有限度的,只有在不改变网的关键路径的情况下,提高关键路径的速度才有效。
- 若网中有几条关键路径,则必须同时提高几条关键路径上的活动速度。
带权有向图中,结点表示事件、边表示活动,权表示开销(如持续时间)。
实际工程中仅存在唯一入度为零的源点和出度为零的汇点。
路径长度:所有活动持续时间之和
关键路径:最长的路径,可能不止一条
关键活动:关键路径上的活动
与计算关键活动有关的量
- 事件Vj最早可能发生的时间(最长路径)
- 活动ai最早可能开始的时间:最长路径
- 事件Vk最迟发生的时间:汇点的最早发生时间减去Vk到汇点的最大路径长度
- 活动ai最迟允许开始时间:最迟完成时间(事件最迟发生时间)减去持续时间
- 时间余量:活动最早可能开始和最迟可能开始的时间余量,关键路径上的活动时间余量为0
利用拓扑分类算法求关键路径和关键活动O(n+e)
- 前进阶段:从源点出发,VE(1)=0;按拓扑序列求其余各项事件最早发生时间VE(k)=max{VE[j]+act[j][k]}(j是k的邻接前序)
- 回退阶段:从汇点出发,VL(n)=VE(n),按逆拓扑有序求其余各点最晚发生时间。VL(j)=min(VL[k]-ACT[j][k])(k是j的所有邻接子代)
- 求每一项活动的最早开始时间E(i)=VE(j)、最晚开始时间VL(k)-ACT[J][K]。若相等,则它是关键活动。