其他
-
有关路径的定义:由顶点和相邻顶点序偶构成的边所形成的序列
-
完全有向图:任意两个顶点之间存在方向相互相反的两条弧
完全无向图:任意两条边之间存在边
-
简单图:不存在顶带你到其自身的边,且同一条边不重复出现
-
若从无向图任一点出发能访问到图中所有顶点,则该图为连通图
-
有向图中两个顶点之间存在相互到达的路径,成两个顶点强连通;
有向图任意两个顶点都强连通,成强连通图;
有向图的底图(无向图)连通,称弱连通;
有向图任意结点之间至少一个可达另一个称单向连通;
-
用相邻矩阵 A 表示图,判定任意两个顶点 Vi 和 Vj 之间是否有长度为 m 的路径相连,则只要检查( )的第 i 行第 j 列的元素是否为零即可。
图的存储方式
图的数组 (邻接矩阵)存储表示
-
一维数组存储顶点信息;N个顶点
二维数组存储顶点之间的逻辑关系 N*N -
无向图顶点的度 = 逻辑矩阵对应行或列的和
无向图一定是一个对称矩阵 -
有向图顶点的 出度 = 逻辑矩阵对应行的和
有向图顶点的 入度 = 逻辑矩阵对应列的和
有向图不一定是一个对称矩阵 -
逻辑矩阵的元素表示两个顶点之间连边的权值 0和无穷表示没有边
#define MAXSIZE 100
typedef struct {
VertexType vexs[MAXSIZE]; //一维数组存放顶点信息
int arcs[MAXSIZE][MAXSIZE]; //逻辑矩阵
int vexnum,arcnum; //顶点树和边数
int kind; //图的类型
}MGraph;
图的邻接表存储表示
-
无向图的度:单链表的长度
-
有向图的出度:单链表的长度
- 入度:每个单链表中该顶点出现的次数
- 一条弧只存一次
-
有必要的情况下:建立有向图的**逆邻接表 **(存的是入度)
typedef struct ArcNode {
int vex; //该弧所指向的顶点位置
struct ArcNode *link; //指向下一条弧的指针
InfoType *info; //该弧相关信息的指针
}ArcNode;
typedef struct VNode {
VertexType data; //顶点信息
ArcNode *firstarc; //指向第一条依附该顶点的弧
}VNode;
typedef struct {
VNode arc[MAXSIZE];
int vexnum,arcnum;
int kind;
}Graphs;
- 邻接表存储网络!!!!!!!!!!!多存一个权值
有向图的十字链表存储表示——存的是 弧
- 一条弧位于两个单链表 ,但只存储一次
typedef struct ArcBox { //弧的结构表示
int tailvex,headvex; //弧头顶点;弧尾顶点
InfoType *info; //弧相关信息
struct ArcBox *hlink,*tlink; //弧头相同的下一条弧hlink;弧尾相同..
}ArcBox;
// 顶点信息
typedef struct VexNode{
VertexType data;
ArcBox *firstin,*firstout; //流入/流出弧的单链表
}VexNode;
typedef struct {
VexNode xlist[MAXSIZE];
int vexnum,arcnum;
}OLGraph;
无向图的邻接多重表存储表示
图的遍历
从一个顶点出发, 访问其余顶点,并且是每个顶点被访问一次
策略:深度优先搜素/广度优先搜素
应用举例:判断图的连通性/求等价类
深度优先搜索
- 当且仅当图的存储和遍历策略确定后,图的遍历序列唯一;否则不唯一。
- 深度优先搜索生成树/森林
流程表示
算法实现
使用邻接矩阵和邻接表都可以;设立访问标志
//从顶点v出发遍历图
void DFS(Graphs G,int v){
printf("%d\t",v);
visited[v]=1; //遍历后的标记
p=G.arc[v].firstarc; //遍历 v 结点的单链表
while(p){
w=p->vex;
if(visited[w]==0) DFS(G,w);//顺藤摸瓜
p=p->link;
}
}
//深度优先搜索遍历
void DFSTraverse(Graphs G){
//初始化与否的标记(使用数组 visited[]存储)
for(v=0;v<G.vexnum;v++)
visited[v]=0;
for(v=0;v<G.vexnum;v++)
if(!visited[v])
DFS(G,v); //对所有为访问的顶点使用DFS函数
}
广度优先遍历
相比“深度…” ,先把同一个顶点连接的顶点遍历完
另一种解读:按照与出发点V0路径长度递增的顺序访问顶点
流程表示
算法实现
int visited[MAX];
//遍历同一个顶点所连的所有结点
void BFS(Graphs G,int v){
int Q[MAX],f=0,r=0; //队列存储当前结点连接的结点
printf("%d\t",v);visited[v]=1;
Q[r++]=v;//当前元素入队
while(f<r){//判断队空与否
x=Q[f++];//出队
//对出队的结点进行搜索
p=G.arc[x].firstarc;
//将当前结点的所连结点存到队列中
while(p){
w=p->vex;
if(visited[w]==0){
visited[w]=1;
printf("%d\t",w);
Q[r++]=w;//入队
}
p=p->link;
}
}
}
//广度优先搜索
void BFSTraverse(Graphs G){
for(v=0;v<G.vexnum;v++)
visited[v]=0
for(v=0;v<G.vexnum;v++)
if(!visited[v]) BFS(G,v);
}
图的连通性问题(无向图)
生成树/生成森林
连通图,n个顶点和e条边,其中n-1条边和n个顶点构成一个极小连通子图,成为生成树
非连通图,各个连通分量的生成树的集合,称为生成森林
注:生成树不唯一
算法实现
- 连通图:任一点出发DSF或者BFS能访问到所有顶点(生成树)
- 非连通图:深度优先搜索生成森林或广度优先搜素生成森林
- 第一个出发点引出第一个生成树,第二个出发点引出第二个生成树
void DFSTree(Graph G, int v, CSTree &T)
{ //v 当前生成树根节点的标号;
//T 当前生成树根节点的地址
visited[v]=1; first=TRUE;
for(p=G.arc[v].firstarc; p!=NULL; p=p->link)
{ //w 当前生成树根节点所连结点的标号
w=p->vex;
if(!visited[w]){
s=(CSTree)malloc(sizeof(CSNode));
s->data=G.arc[w].data; s->fc=NULL; s->nb=NULL;
//当前结点的单链表第一个为左孩子,后边的为兄弟
if (first) {T->fc=s; first=FALSE:}
else T->nb=s;
T=s;
//对 w 结点进行DSF
DFSTree(G, w, T);
}
}
}
void DFSForest(Graph G, CSTree &T)
{ T=NULL;
//初始化访问标记
for(v=0;v<G.vexnum;++v) visited[v]=0;
for(v=0;v<G.vexnum;++v)
//未访问,把该点作为出发点
if(!visited[v]) {
s=(CSTree)malloc(sizeof(CSNode));
s->data=G.arc[v].data; s->fc=NULL; s->nb=NULL;
//将另一条生成树根结点放在上一个生成树根节点的兄弟处(即右孩子)
if(!T) T=s; else q->nb=s;
//保存当前生成树根节点的地址
q=s;
//深度优先搜索生成当前根结点下的生成树
DFSTree(G, v, s); //这里执行的次数就是森林中生成树的个数
}
}
最小生成树(连通图)
定义
带权图的生成树上的各边权值之和称为这棵树的代价。最小代价生成树是各边权值的总和最小的生成树。
注:最小生成树也不是唯一的
MST性质(最小生成树性质)
令G=(V, E, W)为一个带权连通图,T为G的一生成树。对任一不在T中的边uv,如果将uv加入T中会产生一回路,使得uv是回路中权值最大的边。那么树T具有MST性质。
Prim算法
基本步骤
最终 TE 中有 n-1 条边
算法分析
- 从还未在“最小生成树”中的点中进行选择
- 需要知道当前未在生成树中的顶点集合 U
- 需要知道 U 中每个顶点接入“最小生成树”的最小权值 x
- 选择策略:U 中所有顶点 x 最小的那个顶点接入
每个顶点与生成树中顶点相连的最好情况 x
采用一维数组**closedge[MAX]**存放图中每个顶点与生成树中顶点相连的最好情况(筛选权值最小边)
typedef struct{
int adjvex;
int lowcost;
}EdgeType;
EdgeType closedge[MAX];
当v未加入生成树时,closedge[v]存放v与生成树中的顶点相连最好的情况,v与生成树中的顶点的所有连边中权值最小的那条边
- closedge[v].adjvex存放这条边的另一个顶点
- closedge[v].lowcost存放这条边的权值(最好的那种情况)
- closedge[v].lowcost==0表示w加入生成树
算法实现
图采用邻接矩阵和邻接表存放均可
#define MAX 100
#define MAXEDGE 1000000
typedef struct{
int arcs[MAX][MAX];
int vexnum,arcnum;}AGraphs; //邻接矩阵表示
//遍历找出 U 中最好的那个顶点
int minclosedge(EdgeType closedge[])
{ int min,j,k;
min=MAXEDGE;
k=-1;
for(j=0;j<G.vexnum;j++)
if(closedge[j].lowcost!=0 && closedge[j].lowcost<min){
min=closedge[j].lowcost;
k=j;
}
return k;
}
void prim(AGraphs G,int u){
int i,j,k;
EdgeType closedge[MAX];
//存放最小生成树根节点所连结点的权值情况进行存储
for(j=0;j<G.vexnum;j++){
closedge[j].adjvex=u;
closedge[j].lowcost=G.arcs[u][j];
}
//根节点已加入最小生成树
closedge[u].lowcost=0;
for(i=1;i<G.vexnum;i++){
// k 下一个要接入的结点
k=minclosedge(closedge);
printf(“(%d,%d)”, closedge[k].adjvex,k);
closedge[k].lowcost=0;
//重置所有未接入结点j,能够接入树的最小权值 x
for(j=0;j<G.venum;j++)
if(G.arcs[k][j]< closedge[j].lowcost){
closedge[j].lowcost= G.arcs[k][j];
closedge[j].adjvex =k;
}
}
}
事件复杂度:O(n*n)
Prim算法适合于稠密图
Kruskal算法
基本步骤
- 按照边的权值由小到大的顺序考察图的边集中的各边(直至加入
n-1
条边) - 诺被考察边的两个顶点属于T的两个不同连通分量,则将此边作为最小生成树的边加入到T中,同时把两个连通分量连接为一个连通分量;
- 若被考察边的两个顶点属于同一个连通分量,则舍去此边,以免造成回路
- 如此下去,当T中的连通分量个数为1时,此连通分量便为G的一棵最小生成树
算法实现
Kruskal算法适合稀疏图
时间复杂度:O(eloge),e为图的边数
有向无环图及其应用
AOV网
概念
用顶点表示活动,弧表示活动之间的制约(优先)关系
拓扑有序序列(线性序列)
- 已有的优先关系仍存在
- 不存在优先关系的建立一个优先关系
偏序关系/偏序集合
集合A中的二元关系R 自反的,非对称的,传递的
全序关系/全序集合——拓扑排序序列
主要目的
- 判断一个有向图是否包含回路(有回路,说明工程设计不合理)
- 构造拓扑排序:安排活动的进行顺序
测试AOV网是否具有回路——拓扑排序
基本步骤
- 先找入度为零的顶点(无先后要求)
- 删除1找出的顶点及其链接的弧
- 重复1,2操作
- 诺图中还有剩余顶点尾部删除,说明图中有回路
注:一个AOV网的拓扑排序序列可能存在多个,不唯一
拓扑排序算法
- 图采用邻接表存放
- 计算所有顶点的入度,存放与一位数组中
#define MAXSIZE 100
//图的定义相关
typedef struct ArcNode{
int vex;
struct ArcNode *link;
}ArcNode;
typedef struct VNode{
VertexType data;
int id; //顶点的入度
ArcNode * firstarc;
}VNode;
typedef struct {
VNode arc[MAXSIZE];
int vexnum, arcnum;
}Graphs;
//使用队列进行管理
//1.先遍历线性表中入度id为零的所有顶点
//2.从队列开始遍历释放id为零的顶点,并将其对应后序顶点的入度-1(注意判断是否为零)
//3.释放时遍历到队列为空后(两个指针重合),重新遍历线性表id为零的顶点放入队列,重复操作1-2
int topsort(Graphs T){
//count 统计放到拓扑序列中的元素个数
//最终与整个图顶点个数比较,判断是否为 AOV网
int q[MAXSIZE],count=0,h=t=0;
ArcNode *p;
int u,v;
for(v=0;v<T.vexnum;v++)
T.arcs[v].id=0;
//计算所有顶点入度
for(v=0;v<T.vexnum;v++)
for(p=T.arc[v].firstarc; p!=Null;p=p->link){
u=p->vex;
T.arc[u].id++;
}
//将入度为零的顶点放入队列
for (v=0;v<T.vexnum;v++)
if(T.arc[v].id==0) q[t++]=v;
//从队列开始遍历释放id为零的顶点
while(h!=t){
v=q[h++]; printf(“%d”,v); count++;
//对出队顶点的后序顶点的入度-1(注意判断是否为零)
for(p=T.arc[v].firstarc; p!=Null;p=p->link){
u=p->vex; T.arc[u].id--;
//判断为零则入队
if(T.arc[u].id==0) q[t++]=u ;//自己加判断队列是否会溢出!
}
}
//若输出的顶点数小于n,则输出“存在有向回路”
if (count<T.vexnum){printf(“There is a cycle”);return 0;}
else return 1;
}
总的事件复杂度:O(n+e)
对有向无环图利用深度优先搜索进行拓扑排序 DSF
用 DFS 遍历一个无环有向图,并在 DFS 算法退栈返回时打印相应的顶点,则输出的顶点序列是拓扑有序的
AOE网
概念
-
顶点表示事件,弧表示活动,弧上的权值表示完成活动所需时间
-
AOE网应该存在唯一开始顶点,唯一完成顶点
-
只有当前顶点代表的事件发生之后,后续事件才能开始
-
只有进入当前顶点的各条弧代表的活动结束该顶点代表的事件才能发生
研究问题
-
完成整个工程至少需要的时间
- 从开始顶点到完成顶点的最长路径长度
- 该路径即是关键路径,经过的活动就是关键活动
-
影响工程进度的关键活动
最早发生时间
- 一维数组 Ve[]保存每个事件的最早发生时间,也是开始顶点到当前事件的最长路径
- 计算公式(按照拓扑有序求其余顶点的最早…)
事件允许的最晚发生时间(还需要体会)
-
一维数组 Vl[] 保存最晚…
-
在保证完成Ve[n]的前提下,事件 Vi 允许发生的最晚时间
-
活动最早发生时间(与事件相关)
-
一维数组 e[]保存每个活动最早发生时间
-
活动最早发生时间e[j] 等于事件 Vj 可能的最早发生时间Ve[j]
-
活动最晚开始时间 l[i] = Vl[k]-dut(<j,k>)(k 是弧的尾结点)
-
==l[i]-e[i]==就是在不增加完成工作所需的总时间的情况下,活动可以延迟的时间。
诺l[i]=e[i],则活动 Ai为关键活动。;[i]-e[i]>0为不关键事件。
-
关键活动/路径:路径一条即可
-
最短路径
最短路径算法
- 迪杰斯特拉Dijkstra算法——适用从一个源点到其他各点的最短路径
- 弗洛伊德Floyd算法——每对顶点之间的最短路径
Dijkstra算法
注意点
- 按路径长度递增产生最短路径
- 只适用于静态网络
- 网络上边的权值不能为负值
基本思想
注:第一步找到的是伪最短;第二步从伪最短找的最小值是对应顶点的最短,放入集合S
算法实现(还需要理解)
//图的存储
#define max 100
typedef struct {
int arcs[max][max];
int vexnum,arcnum;
}AGraphs;
AGraphs G;
//集合S中点的表示
//方法一
//一维数组表示
//int final[max];
//方法二
//邻接矩阵主对角线的位置表示
//G.arcs[i][i]=0 or 1;
//最短路径的表示
//一维数组表示最短路径的长度
//int D[i]=G.arcs[k][i];
//k是源点
//二维数组表示最短路径包含的顶点
//P[i][j]=0 or 1; Vj 在或不在 从原点到 Vi 的最短路径上
//算法实现
void ShortestPath(AGraphs G,int k,int P[max][max], int D[max]){
int i,w, j,min;
for(i=0;i<G.vexnum;i++){
//初始化
final[i]=0; //集合S点表示
D[i]=G.arcs[k][i]; //初始状态下最短路径长度:不相邻为INFINITY无穷
for(w=0;w<G.vexnum;w++)
P[i][w]=0; //初始化源点k到目标i最短路径包含的顶点
if(D[i]<INFINITY){ //说明 i 结点要末与源点相连,要末就是源点
P[i][k]=1; P[i][i]=1; //源点和相邻的点一定包含在最短路径中
}
}
D[k]=0;
final[k]=1;
for(i=1;i<G.vexnum;i++){
min=INFINITY;
//找到当前到源点的最小点 w ,标记给 j
for(w=0;w<G.vexnum;w++)
if(!final[w]&&D[w]<min){
j=w;
min=D[w];
}
if(min == INFINITY) return;
//添加到最短路径中
final[j]=1;
for(w=0;w<G.vexnum; w ++)
if(!final[w]&&(min+G.arcs[j][w]<D[w])){
D[w]=min+G.arcs[j][w];
P[w]=P[j];
P[w][w]=1;
}
}
}
Floyd算法
基本思路
** n 次试探**
算法实现
void s1(int D[][],int p[][][], Agraphs G){
int i,j,k;
for(i=0;i<G.vexnum;i++)
for(j=0;j<G.vexnum;j++){
D[i][j]=G.arcs[i][j];
for(k=0;k<G.vnum;k++)
p[i][j][k]=0;
if(D[i][j]<INFINITY){
p[i][j][i]=1;
p[i][j][j]=1;
}
}
for(k=0;k<G. vexnum;k++)
for(i=0;i<G.vexnum;i++)
for(j=0;j<G.vexnum;j++)
if(D[i][k]+D[k][j]<D[i][j]){
D[i][j]=D[i][k]+D[k][j];
for(w=0;w<G. vexnum;w++)
p[i][j][w]=p[i][k][w]||p[k][j][w];
}
}
时间复杂度: O(nnn)
**
[外链图片转存中…(img-4PgV4vM4-1717853188513)]
算法实现
[外链图片转存中…(img-DyejeBoF-1717853188513)]
void s1(int D[][],int p[][][], Agraphs G){
int i,j,k;
for(i=0;i<G.vexnum;i++)
for(j=0;j<G.vexnum;j++){
D[i][j]=G.arcs[i][j];
for(k=0;k<G.vnum;k++)
p[i][j][k]=0;
if(D[i][j]<INFINITY){
p[i][j][i]=1;
p[i][j][j]=1;
}
}
for(k=0;k<G. vexnum;k++)
for(i=0;i<G.vexnum;i++)
for(j=0;j<G.vexnum;j++)
if(D[i][k]+D[k][j]<D[i][j]){
D[i][j]=D[i][k]+D[k][j];
for(w=0;w<G. vexnum;w++)
p[i][j][w]=p[i][k][w]||p[k][j][w];
}
}
时间复杂度: O(nnn)