数据结构7---图
一、定义
对于图的定义,我们需要明确几个注意的地方:一线性表中我们把数据元素叫元素,树中叫结点,在途中数据元素我们则称之为顶点(Vertex)。
对于图的定义,我们需要明确几个注意的地方:
- 线性表中我们把数据元素叫元素,树中叫结点,在图中数据元素我们则称之为顶点(Vertex)。
- 线性表可以没有数据元素,称为空表,树中可以没有结点,叫做空树,而图结构在咱国内大部分的教材中强调顶点集合V要有穷非空。
- 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图结构中,任意两个顶点之间都可能有关系,顶点之闻的逻辑关系用边来表示,边集可以是空的。
1、各种名词的定义:
无向边:
若顶点Vi到V之间的边没有方向,则称这条边为无向边(Edge),用无序偶(Vi,Vj)来表示
有向边:
若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),用有序偶<Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。
简单图:
在图结构中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
无向完全图:
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。
有向完全图:
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。
稀疏图和稠密图:
这里的稀疏和稠密是模糊的概念,都是相对而言的,通常认为边或弧数小于n*logn (n是顶点的个数)的图称为稀疏图,反之称为稠密图。
有些图的边或弧带有与它相关的数字,这种与图的边司弧相关的数叫做权(weight),带权的图通常称为网(Network)。
子图:
假设有两个图G1=(V1,E1)和G2=(V2,E2),如果V2cV1,E2EE1,则称G2为G1的子图(Subgraph)。
2、顶点与边的关系
邻接点:
对于无向图G=(V,E),如果边(V1,V2)E,则称顶点V1和V2互为邻接点(Adjacent), 即V1和V2相邻接。边(V1,V2)依附(incident)于顶点V1和V2,或者说边(V1,V2)与顶点V1和V2相关联。
度:
顶点V的度(Degree)是和V相关联的边的数目,记为TD(V),如下图,顶点A与B互为邻接点,边(A,B)依附于顶点A与B上,顶点A的度为3。
邻接到顶点:
对于有向图G=(V,E),如果有<V1,V2>E,则称顶点V1邻接到顶点V2,顶点V2邻接自顶点V1。
度,入度与出度:
以顶点V为头的弧的数目称为V的入度(InDegree),记为ID(V),以V为尾的弧的数目称为V的出度(OutDegree),记为OD(V),因此顶点V的度为TD(V)=ID(V)+OD(V).
路径:
无向图G=(V,E)中从顶点V1到顶点V2的路径(Path)。
如果G是有向图,则路径也是有向的。
下图用红线列举顶点B到顶点D的两种路径,而顶点A到顶点B就不存在路径啦:
路径的长度:
路径的长度是路径上的边或弧的数目。
回路或环:
第一个顶点到最后一个顶点相同的路径称为回路或环(cycle)。
序列中顶点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
3、连通图
在无向图G中,如果从顶点V1到顶点V2有路径,则称V1和V2是连通的,如果对于图中任意两个顶点Vi和Vj都是连通的,则称G是连通图(ConnectedGraph)
连通分量:
无向图中的极大连通子图称为连通分量。注意以下概念:
- 首先要是子图,并且子图是要连通的;
- 连通子图含有极大顶点数;
- 具有极大顶点数的连通子图包含依附于这些顶点的所右边
上图的图1是一个无向非连通图。但是它有两个连通分量,即图2和图3.而图4尽管是图1的子图,但它却不满足连通子图的极大顶点数。因此它不是图1的无向图的连通分量。
强连通图:
在有向图G中,如果对于每一对Vi到Vj都存在路径,则称G是强连通图。
有向图中的极大强连通子图称为有向图的强连通分量。
连通图的生成树:
所谓的一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。
右图就是一个生成树
有向树:
如果一个有向图恰有一个顶点入度为0,其余顶点的入度均为1,则是一棵有向树。
二、图的存储结构
因为任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。
下面有几种存储结构:
(一)邻接矩阵
1、邻接矩阵(无向图)
考虑到图是由顶点和边或弧两部分组成,合在一起比较困难,那就很自然地考虑到分为两个结构来分别存储。
顶点因为不区分大小、主次,所以用一个一维数组来存储是狠不错的选择。
而边或弧由于是顶点与顶点之间的关系,一维数组肯定就搞不定了,那我们不妨考虑用一个二维数组来存储。
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
我们可以设置两个数组,顶点数组为vertex[4]={vO, V1,V2,V3},边数组arc[4][41为对称矩阵(0表示不存在顶点间的边,1表示顶点间存在边)。
对称矩阵:所谓对称矩阵就是n阶矩阵的元满足a[i][j]=a[j][i](0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的。
2、邻接矩阵(有向图)
无向图的边构成了一个对称矩阵,貌似浪费了一半的空间,那如果是有向图来存放,会不会把资源都利用得很好呢?
可见顶点数组vertex[4]={v0, V1,V2,V3},弧数组arc[4][4]也是一个矩阵,但因为是有向图,所以这个矩阵并不对称,例如由V1到V0有弧,得到arc[1][0],而V0到V1没有弧,因此arc[0][1]=0。
另外有向图是有讲究的,要考虑入度和出度,顶点V1的入度为1,正好是第V1列的各数之和,顶点V1的出度为2,正好是第V2行的各数之和。
3、邻接矩阵(网)
在图的术语中,我们提到了网这个概念,事实上也就是每条边上带有权的图就叫网。
这里“”表示一个计算机允许的、大于所有边上权值的值。
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 100
//无向图的邻接矩阵
typedef struct
{char data[MAXSIZE];//顶点数组int arc[MAXSIZE][MAXSIZE];//邻接矩阵int vexnum, edgenum;//当前的定点数和边数}MGraph;
//图的邻接矩阵的创建
void CreateMgrapg(MGraph *p)
{int i, j, k;printf("输入定点数和边数:");scanf("%d %d", &p->vexnum,&p->edgenum);//输入顶点数和边数printf("输入所有顶点的信息:");for (i=0; i<p->vexnum;i++){scanf(" %c",&p->data[i]);}for (i = 0;i < p->vexnum;i++){for (j = 0;j<p->vexnum;j++){p->arc[i][j] = 0;}}for (k=0; k < p->edgenum; k++){printf("输入第%d边的两个顶点的序号,格式(i j):",k+1);scanf("%d %d", &i, &j);p->arc[i][j] = 1;p->arc[j][i] = 1;//去掉这个可以表示有向图}
}
void print(MGraph g)
{int i,j;for (i = 0; i < g.vexnum; i++){for (j = 0; j < g.vexnum; j++){printf("%-3d", g.arc[i][j]);}printf("\n");}
}
int main()
{MGraph g;CreateMgrapg(&g);print(g);return 0;
}
(二)邻接表
1、邻接表(无向图)
把数组与链表结合一起来存储,这种方式在图结构也适用,我们称为邻接表(AdjacencyList)。
邻接表的处理方法是这样:
- 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。
- 图中每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数不确定,所以我们选择用单链表来存储。
2、邻接表(有向图)
若是有向图,邻接表结构也是类似的,我们先来看把顶点当弧尾建立的邻接表,这样很容易就可以得到每个顶点的出度:
但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表:
此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。
3、邻接表(网)
对于带权值的网图,可以在边表结点定义中再增加一个数据域来存储权值即可:
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
//有向图邻接表的存储
typedef char VertexType;
typedef struct node{//定义边表结点int adjvex;//邻接点域struct node *next;//指向下一个邻接点域的指针域
}EdgeNode;
typedef struct vexnode{//定义顶点表结点VertexType data;//顶点域EdgeNode *firstedge;//指向第一条边结点
}VHeadeNode;
typedef struct{VHeadeNode adjlist[MAXSIZE];/*邻接表头结点数组*/int n,e;//顶点数,边数
}AdjList;//有向图邻接表的创建
void GreateAGraph(AdjList *g){int i,j,k;printf("请输入图的顶点数和边数:");scanf("%d %d",&g->n,&g->e);printf("请输入图的所有顶点信息:");for(i=0;i<g->n;i++){scanf(" %c",&(g->adjlist[i].data));//给图的每个结点的顶点域赋值g->adjlist[i].firstedge = NULL;//首先点的边表头指针都设为空}EdgeNode *p;for(k=0;k<g->e;k++){printf("请输入第%d条边对应的两个顶点的序号,格式(i j):",k+1);scanf("%d %d",&i,&j);p = (EdgeNode*)malloc(sizeof(EdgeNode));p->adjvex = j;p->next = g->adjlist[i].firstedge;//头插法g->adjlist[i].firstedge = p;}
}
//邻接表的输出
void printadj(AdjList *g){int i;EdgeNode *p;for(i=0;i<g->n;i++){printf("%2d [%c]",i,g->adjlist[i].data);p = g->adjlist[i].firstedge;while(p!=NULL){printf("-->[%d]",p->adjvex);p = p->next;}printf("\n");}
}
int main() {AdjList G;GreateAGraph(&G);//创建有向图printadj(&G);return 0;
}
(三)十字链表(有向图)
结构
即:弧起点的下标 弧终点的下标 入度的指针 出度的指针
蓝线:出度 红线:入度
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以Vi为尾的弧,也容易找到以Vi为头的弧,因而容易求得项点的出度和入度。
(四)邻接多重表
结构
其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。iLink指向依附顶点iVex的下一条边,jLink指向依附顶点jVex的下一条边。
也就是说在邻接多重表里边,边表存放的是一条边,而不是一个顶点。
(五)边集数组
边集数组是由两个一维数组构成,一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
三、图的遍历
谈到图的遍历,那就复杂多了,因为它的任一顶点都可以和其余的所有顶点相邻接,因此极有可能存在重复走过某个顶点或漏了某个顶点的遍历过程。
对于图的遍历,如果要避免以上情况,那就需要科学地设计遍历方案,通常有两种遍历次序方案:化们是深度优先遍历(DFS)和广度优先遍历(BFS)。
(一)深度优先遍历
深度优先遍历(DepthFirstSearch),也有称为深度优先搜索,简称为DFS。
我们可以约定右手原则:在没有碰到重复顶点的情况下,分叉路口始终是向右手边走,每路过一个顶点就做一个记号。
这里规定的就是去小的,当然也可以规定先去大的
那么如何实现呢?
1、邻接矩阵(递归)
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 //最大顶点数
#define MAX_INT 32767//设置最大值
typedef struct{char vexs[MAXSIZE];//这里的数据类型根据实际情况而定int arcs[MAXSIZE][MAXSIZE];//这里的数据类型根据实际情况而定int vexnum, arcnum;//图的当前顶点数和边数
}Graph;
int get_index(char* arr,char m)
{int i = 0;for(i = 0; i < MAXSIZE; i++){if(arr[i] == m)return i;}return -1;
}void CreatGraph(Graph* G)
{int i,j = 0;printf("请输入顶点和边的数量:>");scanf("%d%d",&G->vexnum,&G->arcnum);//把输入的值保存到图结构体中for(i = 0; i < G->vexnum; i++)//初始化邻接矩阵{for(j = 0; j < G->vexnum; j++){G->arcs[i][j] = 0;}}for(i = 0; i < G->vexnum; i++){printf("请输入每个顶点的值:>");getchar();//清除键盘缓存区的回车键scanf("%c", &G->vexs[i]);//把输入的值保存到顶点数组当中}for(i = 0; i < G->arcnum; i++)//有几条边,循环几次{char m,n;//用来接收两个顶点int j,k;//接收顶点的下标printf("请依次输入每一条边,格式为:ab >");getchar();scanf("%c%c",&m,&n);j = get_index(G->vexs,m);//得到输入的m的值,在顶点数组中的下标k = get_index(G->vexs,n);//得到输入的n的值,在顶点数组中的下标G->arcs[j][k] = 1;//给邻接矩阵对应的位置赋权值G->arcs[k][j] = 1;//因为是无向网,所以是对称的,给对称点赋相同值}}
//深度遍历创建的无向图
void DepthSearch(Graph G, int v, int*visited)//参数为创建的图,输入的值在数组中的下标,判断是否被访问过的数组
{int i = 0;visited[v] = 1;printf("%c", G.vexs[v]);for(i = 0; i < G.vexnum; i++)//遍历二维数组v行的每一列{if((G.arcs[v][i] == 1)&&(visited[i]!=1))//如果有边相连,而且该顶点还没有被访问过DepthSearch(G, i,visited);//递归搜索该顶点}
}
int main()
{Graph G;CreatGraph(&G);char input;int visited[MAXSIZE] = {0};//创建数组,用来判断某一个顶点是否被访问过printf("请输入搜索的起始点:>");getchar();scanf("%c",&input);DepthSearch(G, get_index(G.vexs, input),visited);return 0;}
2、邻接表
代码与上面邻接矩阵也差不多,只是在条件判断时有所不同,这里不需要判断表结点是不是顶点的边,只需要判断表结点是不是空.递归具体代码如下:
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 //最大顶点数
#define MAX_INT 32767//设置最大值
typedef struct TableNode{//表结点int index;//结点在数组中的下标struct TableNode* nextarc;int info;//权值}TNode;typedef struct{//头结点char data;TNode* firstarc;
}HNode;typedef struct{//整个无向网的结构体HNode* head;int vexnum,arcnum;
}Gragh;
int get_index(HNode* arr,char m)
{int i = 0;for(i = 0; i < MAXSIZE; i++){if(arr[i].data == m)return i;}return -1;
}void CreatGraph(Gragh* G)
{int i = 0;printf("请输入顶点和边的数量:>");scanf("%d%d",&G->vexnum,&G->arcnum);//把输入的值保存到图结构体中for(i = 0; i < G->vexnum; i++){printf("请输入每个顶点的值:>");getchar();//清除键盘缓存区的回车键scanf("%c", &G->head[i].data);//把输入的值保存到顶点数组当中G->head[i].firstarc = NULL;}for(i = 0; i < G->arcnum; i++)//有几条边,循环几次{char m,n;//用来接收两个顶点int j,k;//接收顶点的下标printf("请依次输入每一条边,格式为:ab >");getchar();scanf("%c%c",&m,&n);j = get_index(G->head,m);//得到输入的m的值,在顶点数组中的下标k = get_index(G->head,n);//得到输入的n的值,在顶点数组中的下标TNode* P1 = malloc(sizeof(TNode));P1->index = k;P1->nextarc = G->head[j].firstarc;G->head[j].firstarc = P1;//因为是无向图,所以还要建立一条反向的边TNode* P2 = malloc(sizeof(TNode));P2->index = j;P2->nextarc = G->head[k].firstarc;G->head[k].firstarc = P2;}}
//深度遍历创建的无向图
void DepthSearch(Gragh G, int v, int*visited)//参数为创建的图,输入的值在数组中的下标,判断是否被访问过的数组
{visited[v] = 1;printf("%c", G.head[v].data);TNode* P = G.head[v].firstarc;while(P){if(!visited[P->index])DepthSearch(G, P->index, visited);//如果表结点不为空且判断数组值不为1,则递归搜索该结点P = P->nextarc;//指针指向该结点的下一个结点}}
int main()
{Gragh G;CreatGraph(&G);char input;int visited[MAXSIZE] = {0};//创建数组,用来判断某一个顶点是否被访问过printf("请输入搜索的起始点:>");getchar();scanf("%c",&input);DepthSearch(G, get_index(G.head, input),visited);return 0;}
非递归
void DFS(Graph g,int v){//采用非递归的方式实现图的深度优先遍历InitStack(&s);//创建并初始化栈sPUSH(&s,v);//待访问结点入栈while(!IsEmpty(s)){//栈非空栈顶元素出栈并访问,同时让其所有未被访问的邻结点入栈POP(&s,&v);visit(v);visited[v]=1;int w=FirstAdjVex(g,v);while(w!=-1){if(!visited[w]){PUSH(&s,w);}w=NextAdjVertex(g,v,w);}
时间效率
实例(马踏棋盘算法(骑士周游问题))
国际象棋的棋盘为8*8的方格棋盘,现将“马”放在任意指定的方格中,按照“马”走棋的规则将“马”进行移动。要求每个方格只能进入一次,最终使得“马”走遍棋盘64个方格。
编写代码,实现马踏棋盘的操作,要求用1~64来标注“马”移动的路径(看演示)。
对于在n*n的棋盘上,当n>=5且为偶数的时候,以任意点作点都有解。
回溯法
一之前我们谈过回溯法,还是那句话,指导思想很简单,就是一条路走到黑,碰壁了再回来一条路走到黑......一般和递归可以很好的搭配使用,还有深度优先搜索(DFS) 。
哈密尔顿路径:
一图G中的哈密尔顿路径指的是经过图G中每个顶点,且只经过一次的一条轨迹。如果这条轨迹是一条闭合的路径(从起点出发不重复地遍历所有点后仍能回到起始点),那么这条路径称为哈密尔顿回路
6 | 2 | |||
4 | 0 | |||
当前位置 | ||||
5 | 1 | |||
7 | 3 |
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>#define X 6
#define Y 6int chess[X][Y];//找到基于(x,y)位置下一个可走的位置int nextxy(int* x, int* y, int count)//传入x,y的地址,可以像全局变量一样修改变量的值。
{switch (count){case 0:if (*x + 2 <= X - 1 && *y - 1 >= 0 && chess[*x + 2][*y - 1] == 0){*x += 2;*y -= 1;return 1;}break;case 1:if (*x + 2 <= X - 1 && *y + 1 <= Y - 1 && chess[*x + 2][*y + 1] == 0){*x += 2;*y += 1;return 1;}break;case 2:if (*x + 1 <= X - 1 && *y + 2 <= Y - 1 && chess[*x + 1][*y + 2] == 0){*x += 1;*y += 2;return 1;}break;case 3:if (*x - 1 >= 0 && *y + 2 <= Y - 1 && chess[*x - 1][*y + 2] == 0){*x -= 1;*y += 2;return 1;}break;case 4:if (*x - 2 >= 0 && *y + 1 <= Y - 1 && chess[*x - 2][*y + 1] == 0){*x -= 2;*y += 1;return 1;}break;case 5:if (*x - 2 >= 0 && *y - 1 >= 0 && chess[*x - 2][*y - 1] == 0){*x -= 2;*y -= 1;return 1;}break;case 6:if (*x - 1 >= 0 && *y - 2 >= 0 && chess[*x - 1][*y - 2] == 0){*x -= 1;*y -= 2;return 1;}break;case 7:if (*x + 1 <= X - 1 && *y - 2 >= 0 && chess[*x + 1][*y - 2] == 0){*x += 1;*y -= 2;return 1;}break;}return 0;
}void print() {int i, j;for (i = 0; i < X; i++) {for (j = 0; j < Y; j++) {printf("%2d\t", chess[i][j]);}printf("\n");}printf("\n");
}//DFS 深度优先遍历棋盘
//(x,y)为位置坐标
//tag是标记变量,每走一步,tag + 1
int TravelChessBoard(int x, int y, int tag) {int x1 = x, y1 = y, flag = 0, count = 0;chess[x][y] = tag;if (X * Y == tag) {print();return 1;}//找到马的下一个可走坐标(x1,y1),如果找到flag = 1,否则为0flag = nextxy(&x1, &y1, count);while (0 == flag && count < 7) {count++;flag = nextxy(&x1, &y1, count);}while (flag) {if (TravelChessBoard(x1, y1, tag + 1)) {return 1;}//继续找到马的下一步可走的坐标(x1,y1)...如果找到flag = 1,否则为0//前面的八种情况都不行x1 = x;//重新赋值是因为上面寻找的时候nextadress已经对x1,y1做改变了y1 = y;//退回到前一格(传入nextadress的一格)(回溯1)count++;flag = nextxy(&x1, &y1, count);while (0 == flag && count < 7) {count++;flag = nextxy(&x1, &y1, count);}}if (0 == flag) {chess[x][y] = 0;return 0;}
}int main() {int i, j;clock_t start, finish;start = clock();for (i = 0; i < X; i++) {for (j = 0; j < Y; j++) {chess[i][j] = 0;}}if ( TravelChessBoard(2, 0, 1)==0) {printf("fail\n");}finish = clock();printf("\n本次计算一共花费:%f秒\n\n", (double)(finish - start) / CLOCKS_PER_SEC);return 0;
}
emmm,吃饭去了,回溯的理解,DFS把8个情况可能都走一遍
(二)广度优先遍历
广度优先遍历(BreadthFirstSearch),又称为广度优先搜索,简称BFS。
1、邻接表
#include<stdlib.h>
#include<stdio.h>
#include<malloc.h>
#include<string.h>
//图的邻接表类型定义
typedef char VertexType[4];
typedef char InfoPtr;
typedef int VRType;
#define MaxSize 50 //最大顶点个数
typedef enum { DG, DN, UN, UG } GraphKind; //图的类型:有向图、有向网、无向图和无向网
typedef struct ArcNode {//边表结点的类型定义 int adjvex;// 弧(边)指向的顶点的位置InfoPtr* info;//与弧(边)相关的信息 struct ArcNode* nextarc;//指示下一个与该顶点相邻接的顶点
} ArcNode;
typedef struct VNode { //表头结点的类型定义 VertexType data;//用于存储顶点ArcNode* firstarc;//指示第一个与该顶点邻接的顶点
}VNode, AdjList[MaxSize];
typedef struct { //图的类型定义 AdjList vertex;int vexnum, arcnum;//图的顶点的数目与弧的数目 GraphKind kind;//图的类型
}AdjGraph;
//函数声明
int LocateVertex(AdjGraph G, VertexType v);
void CreateGraph(AdjGraph* G);
void DisplayGraph(AdjGraph G);
void DestroyGraph(AdjGraph* G);
//深度
void DFSTraverse(AdjGraph G);
void DFS(AdjGraph G, int v);
int FirstAdjVertex(AdjGraph G, VertexType v);
int NextAdjVertex(AdjGraph G, VertexType v, VertexType w);void BFSTraverse(AdjGraph G);
void CreateGraph(AdjGraph* G) {int i, j, k;VertexType v1, v2;//定义两个顶点v1和v2ArcNode* p;printf("请输入图的顶点数,边数(逗号分割):");scanf("%d,%d", &(*G).vexnum, &(*G).arcnum);printf("请输入%d个顶点的值:\n", G->vexnum);for (i = 0; i < G->vexnum; i++) {scanf("%s", G->vertex[i].data);G->vertex[i].firstarc = NULL;}printf("请输入弧尾和弧头(以空格作为间隔);\n");for (k = 0; k < G->arcnum; k++) {scanf("%s%s", v1, v2);i = LocateVertex(*G, v1);j = LocateVertex(*G, v2);//j为入边,i为出边 创建邻接表p = (ArcNode*)malloc(sizeof(ArcNode));p->adjvex = j;//邻接点域 p->info = NULL;//数据域 p->nextarc = G->vertex[i].firstarc;//指针域G->vertex[i].firstarc = p;//i为入边,j为出边 创建邻接表p = (ArcNode*)malloc(sizeof(ArcNode));p->adjvex = i;//邻接点域 p->info = NULL;//数据域 p->nextarc = G->vertex[i].firstarc;//指针域G->vertex[j].firstarc = p;}(*G).kind = UG;
}
//查找
int LocateVertex(AdjGraph G, VertexType v) {int i;for (i = 0; i < G.vexnum; ++i) {if (strcmp(G.vertex[i].data, v) == 0) {return i;}}return -1;
}
//销毁无向图
void DestroyGraph(AdjGraph* G) {int i;ArcNode* p, * q;for (i = 0; i < (*G).vexnum; ++i) {//释放图中的边表结点 p = G->vertex[i].firstarc;//p指向边表的第一个结点 if (p != NULL) {//如果边表不为空,则释放边表的结点 q = p->nextarc;free(p);p = q;}}(*G).vexnum = 0;//将顶点的数目为0 (*G).arcnum = 0;//将边的数目为0
}
//打印
void DisplayGraph(AdjGraph G) {int i;ArcNode* p;printf("%d个顶点:\n", G.vexnum);for (i = 0; i < G.vexnum; i++) {printf("%s", G.vertex[i].data);}printf("\n%d条边:\n", 2 * G.arcnum);for (i = 0; i < G.vexnum; i++) {p = G.vertex[i].firstarc;//将p指向边表的第一个结点while (p) {printf("%s->%s ", G.vertex[i].data, G.vertex[p->adjvex].data);p = p->nextarc;}printf("\n");}
}
int visited[MaxSize];
//打印深度优先遍历的数据
void Visit(VertexType v) {printf("%s ", v);
}
//深度
void DFSTraverse(AdjGraph G) {int v;for (v = 0; v < G.vexnum; v++) {visited[v] = 0; //访问标志数组初始化为未被访问 }printf("\n深度优先遍历序列:\n");for (v = 0; v < G.vexnum; v++) {if (!visited[v]) {DFS(G, v); //对未访问的顶点v进行深度优先遍历 }}printf("\n");
}
//从顶点v出发递归深度优先遍历
void DFS(AdjGraph G, int v) {int w;visited[v] = 1;//访问标志设置为已访问 Visit(G.vertex[v].data);for (w = FirstAdjVertex(G, G.vertex[v].data); w > 0; w = NextAdjVertex(G, G.vertex[v].data, G.vertex[w].data))if (!visited[w])DFS(G, w);//递归调用DFS对v的尚未访问的序号为w的邻接顶点
}
//返回顶点v的第一个邻接顶点的序号
int FirstAdjVertex(AdjGraph G, VertexType v) {ArcNode* p;int v1;v1 = LocateVertex(G, v);//v1为顶点v在图G中的序号 p = G.vertex[v1].firstarc;if (p) //如果顶点v的第一个邻接点的存在,返回邻接点的序号,否则返回-1 return p->adjvex;elsereturn -1;
}
//返回v的相对于w的下一个临界顶点的序号
int NextAdjVertex(AdjGraph G, VertexType v, VertexType w) {ArcNode* p, * next;int v1, w1;v1 = LocateVertex(G, v); //v1为顶点v在图G中的序号 w1 = LocateVertex(G, w); //w1为顶点v在图G中的序号 for (next = G.vertex[v1].firstarc; next;) {if (next->adjvex != w1) {}next = next->nextarc;}p = next; //p指向顶点v的邻接顶点w的结点 if (!p || p->nextarc) {//如果w不存在或w是最后一个邻接点,则返回-1 return -1;}else {return p->nextarc->adjvex;//返回v的相对于w的下一个邻接点的序号 }
}//广度
void BFSTraverse(AdjGraph G) {printf("\n广度优先遍历序列:\n");int v, u, w, front, rear;ArcNode* p;int queue[MaxSize];//定义一个队列Q‘front = rear = -1;//初始化队列Qfor (v = 0; v < G.vexnum; v++) {visited[v] = 0;}v = 0;visited[v] = 1; //设置访问标志为1,表示已经被访问过 Visit(G.vertex[v].data);rear = (rear + 1) % MaxSize;queue[rear] = v; //v入队 while (front < rear) { //如果队列不为空 front = (front + 1) % MaxSize;v = queue[front]; //队头元素出队赋值给v p = G.vertex[v].firstarc;while (p != NULL) { //遍历序号为v的所有邻接点 if (visited[p->adjvex] == 0) {//如果该顶点未被访问过 visited[p->adjvex] = 1;Visit(G.vertex[p->adjvex].data);rear = (rear + 1) % MaxSize;queue[rear] = p->adjvex;}p = p->nextarc;//p指向下一个邻接点 }}
}
int main() {AdjGraph G;printf("采用邻接表创建无向图G:\n");CreateGraph(&G);printf("输出无向图G:");DisplayGraph(G);DFSTraverse(G);BFSTraverse(G);DestroyGraph(&G);return 1;
}
这里比较好理解
就是说利用队列来帮助实现
2、邻接矩阵
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#define MVNUM 100 //最大顶点数
#define MAXINT 32767 //极大值相当于无穷大int visited[MVNUM] = { 0 }; //辅助数组,判断遍历过了没
int visit[MVNUM] = { 0 }; //同理
typedef struct
{char vexs[MVNUM]; //顶点数据数组int arr[MVNUM][MVNUM]; //邻接矩阵int vexnum, arcnum; //现有顶点数与边数
}AMGraph;
typedef struct
{int* base; //队列数组int front; //队头的下标int rear; //队尾的下标
}sqQueue;int initGraph(AMGraph& G); //初始化邻接矩阵
void showGraph(AMGraph G); //打印邻接矩阵
int locatVex(AMGraph G, char u); //定位顶点在邻接矩阵的下标
int createGraph(AMGraph& G); //建立邻接矩阵
void dfsAM(AMGraph G,int i); //深度优先搜索遍历
void bfsAM(AMGraph G, int i); //广度优先搜索遍历
int initQueue(sqQueue& Q); //初始化队列
int enQueue(sqQueue& Q, int i); //入队
int firstVEX(AMGraph G, int u); //第一个邻接顶点
int nextVEX(AMGraph G,int w ,int u); //下一个邻接顶点int main()
{AMGraph G; initGraph(G);createGraph(G);showGraph(G);cout << "the result of dfs is:";dfsAM(G,0);cout << endl;cout << "the result of bfs is:";bfsAM(G,0);
}
int initGraph(AMGraph& G)
{cout << "please input some vexnum and arcnum!" << endl;cin >> G.vexnum >> G.arcnum; //输入你想要的顶点数和边数cout << "please input data of vex!" << endl;for (int i = 0; i < G.vexnum; i++){cin >>G.vexs[i]; //输入顶点数据}for (int i = 0; i < G.vexnum; i++){for (int j = 0; j < G.vexnum; j++){G.arr[i][j] = MAXINT; //邻接矩阵的初值都为无穷大} }return 1;
}
void showGraph(AMGraph G)
{for (int i = 0; i < G.vexnum; i++){for (int j = 0; j < G.vexnum; j++){if (G.arr[i][j] == MAXINT) //无穷大弄得更好看点cout << "∞" << " ";elsecout << " " << G.arr[i][j] << " ";}cout << endl;}cout << endl;
}int locateVex(AMGraph G, char u)
{for (int i = 0; i < G.vexnum; i++) {if (u == G.vexs[i]) //如果u的值和顶点数据匹配,就返回顶点在矩阵中的下标return i;}return -1;
}int createGraph(AMGraph& G)
{int i = 0; int j = 0;int w = 0; //i,j代表顶点下标,w代表权值char v1 = 0; char v2 = 0; //v1,v2为顶点数据cout << "please input w of v1 to v2 in graph!" << endl;for (int k = 0; k < G.arcnum; k++){cin >> v1 >> v2 >> w; i = locateVex(G, v1); //找到v1在顶点表的下标,并返回赋值给ij = locateVex(G, v2);G.arr[i][j] = w;G.arr[j][i] = G.arr[i][j]; //无向图的矩阵是对称矩阵}cout << endl;return 1;
}void dfsAM(AMGraph G, int i)
{//随机选一个顶点下标,这里为0cout << G.vexs[i]<<" "; //输出0下标在顶点表的值 visited[i] = 1; //辅助数组对应下标i的值置为1for (int j = 0; j < G.vexnum; j++){if (G.arr[i][j] != MAXINT && (!visited[j])) //只要是邻接的顶点并且没有访问过{ //不然就退回,也是递归结束条件dfsAM(G, j); //递归使用函数}}
}
int initQueue(sqQueue& Q)
{Q.base = (int *)malloc(sizeof(int) * MVNUM); //给base动态分配一个int*类型MVNUM个int大小的一维数组空间Q.front = Q.rear = 0; //队头和对尾下标都置为0return 1;
}
int enQueue(sqQueue& Q, int i)
{if ((Q.rear + 1) % MVNUM == Q.front) //队满return 0;Q.base[Q.rear] = i; //先赋值再加Q.rear = (Q.rear + 1) % MVNUM;return 1;
}int deQueue(sqQueue& Q, int &u)
{if (Q.rear == Q.front) //队空return 0;u = Q.base[Q.front]; //要删的值给u然后再加Q.front = (Q.front + 1) % MVNUM;return 1;
}int firstVEX(AMGraph G, int u)
{//在邻接矩阵u行0列开始遍历,如果找到不等于无穷的,
//并且没有访问过的就返回列的下标,否则就返回无穷for (int i = 0; i < G.vexnum; i++) {if (G.arr[u][i] != MAXINT && visit[i] == 0) {return i;}}return MAXINT;
}int nextVEX(AMGraph G, int w, int u)
{//在邻接矩阵u行w+1列开始遍历,如果找到不等于无穷的,
//并且没有访问过的就返回列的下标,否则就返回无穷for (int i = w + 1; i < G.vexnum; i++){if (G.arr[u][i] != MAXINT && visit[i] == 0){return i;} }return MAXINT;
}void bfsAM(AMGraph G, int i)
{//随机选一个顶点下标,这里为0cout << G.vexs[i] << " "; //输出i下标在顶点表的值 visit[i] = 1; //辅助数值对应下标i的值置为1sqQueue Q; initQueue(Q);enQueue(Q, i); //i为矩阵中顶点的行下标,让它入队(顶点表的下标和矩阵的列下标,行下标一致,本算法中说谁的下标都一样)while (Q.rear != Q.front) //队不为空{int u = 0; //接收矩阵中顶点的行下标,因为是邻接矩阵deQueue(Q,u); //出队并让u接收矩阵中顶点的行下标for (int w = firstVEX(G, u); w != MAXINT; w = nextVEX(G, w, u)){//注意在一次循环中u不变if (!visit[w]){cout << G.vexs[w] << " ";visit[w] = 1;enQueue(Q, w);}}}}
思路最重要吧
与DFS一致
四、最小生成树
图的生成树:图中所有顶点均由边链接在一起,但不存在回路的图。
一个图可以有许多棵生成树
无向图的生成树
最小生成树(Minimum Spanning Tree):给定一个无向网,在该网所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。
(一)普利姆算法
不想写了
代码如下
# include <iostream>
# define SIZE 20
# define NEWSIZE 20
# define maxn 100000000 //表示无限大
using namespace std;
typedef struct ArcNode { //边的结点结构类型int adjvex; //该边的终点编号int weight; //该边的权值struct ArcNode* nextarc; //指向下一条边的指针
}ArcNode;
typedef struct VexNode { //顶点结构char data;ArcNode* firstarc; //指向第一条与该顶点有关的边的指针
}VexNode;
typedef struct Graph { //邻接表结构类型VexNode* VNode; //定义邻接表int vexnum, arcnum; //顶点数和边的个数int size; //邻接表的大小
}Graph;int* Adjvex; //保存在最小生成树中的顶点
int* Lowcost; //保存不在最小生成树中的各点到最小生成树中的点的边的最小权值
int* Flag; //标注该点是否已加入最小生成树void Create_Graph(Graph& G) //创建图(邻接表)
{cout << "顶点的个数:";cin >> G.vexnum;G.VNode = (VexNode*)malloc(SIZE * sizeof(VexNode));G.size = SIZE;while (G.size <= G.vexnum) { //根据点的个数动态分配空间G.VNode = (VexNode*)realloc(G.VNode, (G.size + NEWSIZE) * sizeof(VexNode));G.size += NEWSIZE;}Adjvex = (int*)malloc((G.size + 10) * sizeof(int));Lowcost = (int*)malloc((G.size + 10) * sizeof(int));Flag = (int*)malloc((G.size + 10) * sizeof(int));cout << "请输入各顶点的名称:";for (int i = 1; i <= G.vexnum; i++) {cin >> G.VNode[i].data;G.VNode[i].firstarc = NULL; //邻接表初始化,所有单向链表均为空表}cout << "请输入边的个数:";cin >> G.arcnum;cout << "请输入各边起点和终点的编号及权重:" << endl;int x, y, w; //x:起始点,y:终点,w:权重ArcNode* p, * q;for (int i = 1; i <= G.arcnum; i++) {cin >> x >> y >> w;p = (ArcNode*)malloc(sizeof(ArcNode)); //创建一个用于存放当前边的结点pp->nextarc = NULL;p->adjvex = y;p->weight = w;q = G.VNode[x].firstarc;//将边按顺序插入到链表末尾if (q == NULL) {G.VNode[x].firstarc = p;}else {while (q->nextarc != NULL) {q = q->nextarc;}q->nextarc = p;}p = (ArcNode*)malloc(sizeof(ArcNode));p->nextarc = NULL;p->adjvex = x;p->weight = w;q = G.VNode[y].firstarc;if (q == NULL) {G.VNode[y].firstarc = p;}else {while (q->nextarc != NULL) {q = q->nextarc;}q->nextarc = p;}}
}void MinSpanningTree_Prim(Graph& G, int v) //从顶点v出发求图的最小生成树
{for (int i = 1; i <= G.vexnum; i++) { //初始化Adjvex[i] = v;Lowcost[i] = maxn;Flag[i] = 0;}Lowcost[v] = 0; //初始点对应的权置为0Flag[v] = 1; //标记初始点(即将初始点加入树中)cout << G.VNode[v].data; //输出初始点的值int num = 1; //记录目前最小生成树中的点的数目ArcNode* p;while (num < G.vexnum) {p = G.VNode[v].firstarc; //p为新加入树的点的第一个邻接点while (p != NULL) { //由于新点加入,更新各不在最小生成树中的点到最小生成树中点的最小距离if (!Flag[p->adjvex] && Lowcost[p->adjvex] > p->weight) {Lowcost[p->adjvex] = p->weight;Adjvex[p->adjvex] = v;}p = p->nextarc;}int min = maxn;for (int i = 1; i <= G.vexnum; i++) { //寻找目前到最小生成树距离最小的点(该点不在最小生成树中)if (!Flag[i] && Lowcost[i] < min) {min = Lowcost[i];v = i; //更新v为目前到最小生成树距离最小的点(该点不在最小生成树中)}}Flag[v] = 1; //标记该点(即将该点加入最小生成树中)cout << G.VNode[v].data; //输出新加入树的点的值num++; //最小生成树中的点的数目+1}
}int main()
{Graph G;Create_Graph(G);int sum = 0;cout << "最小生成树为:";MinSpanningTree_Prim(G, 1); //从1号点出发cout << endl;for (int i = 1; i <= G.vexnum; i++) {cout << Adjvex[i] << " ";}cout << endl;for (int i = 1; i <= G.vexnum; i++) {cout << Lowcost[i] << " ";sum += Lowcost[i]; //求最小生成树的值}cout << endl;for (int i = 1; i <= G.vexnum; i++) {cout << Flag[i] << " ";}cout << endl;cout << "最小生成树的值为:" << sum << endl;return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define INF INT_MAX// 邻接矩阵表示的无向图
typedef struct {int V; // 顶点数int E; // 边数int G[100][100]; // 邻接矩阵
} Graph;// 获取边的权重
int getWeight(Graph G, int u, int v) {return G[u][v];
}// Kruskal算法求最小生成树
Graph primMST(Graph G) {int V = G.V; // 顶点数int E = G.E; // 边数int parent[V]; // 父节点数组int dist[V]; // 从源点到每个顶点的距离数组int i, u, v;int minCost = 0; // 总代价int edgeCount = 0; // 已选边的数量Graph mstEdges; // MST边集合memset(parent, -1, sizeof(parent)); // 初始化父节点为-1memset(dist, INF, sizeof(dist)); // 初始化距离为正无穷大priorityQueueNode pq; // 优先队列头结点pq.data = (void*)&dist[0];pq.index = 0;memset(mstEdges.G, 0, sizeof(mstEdges.G)); // 将邻接矩阵清零mstEdges.V = V;mstEdges.E = 0;do { // 不断扩展最小生成树,直到不存在增广路为止u = pq.index; // 取出距离源点最近的顶点uif (u == -1) break; // 如果已经没有顶点可选了,跳出循环pq.index = parent[u]; // 将当前顶点更新为其父节点edgeCount++; // 已选边的数量加1for (i = 0; i < G.V; i++) { // 遍历所有顶点v = i; // 从当前顶点开始选择下一个顶点vif (dist[v] > dist[u] + G.G[u][v]) { // 如果从u到v的距离比从u到源点的距离更短,则更新距离和优先级队列头结点dist[v] = dist[u] + G.G[u][v];pq.data = (void*)&dist[0];pq.index = i;} else if (i != u && v != u) { // 如果当前顶点u不是目标顶点,且从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并更新优先级队列头结点的位置mstEdges.G[edgeCount] = getWeight(G, u, v);pq.data = (void*)&mstEdges.G[0];pq.index = edgeCount++;} else if (i == u && v != u) { // 如果当前顶点u是目标顶点,但从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并将当前顶点更新为其父节点的值为已选边的数量减一(因为此时已经找到了一条增广路径)mstEdges.G[edgeCount] = getWeight(G, u, v);parent[v] = edgeCount--; // 将当前顶点的父节点设为已选边的数量减一(因为此时已经找到了一条增广路径)} else if (i != u && v == u) continue; // 如果当前顶点u是目标顶点且从u到v的距离等于从u到源点的距离,则不需要进行任何操作,直接跳过本次循环继续下一次循环迭代}minCost += mstEdges.G[mstEdges.E-1]; // 将总代价加上已选边的权重之和作为新的总代价mstEdges.E++; // 将已选边的计数加一,表示又选了一条边加入到最小生成树中} while (edgeCount < V); // 当已选边的数量小于顶点数时,继续扩展最小生成树直到不存在增广路为止
时间复杂度O()
(二)克鲁斯卡尔算法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define INF INT_MAX// 邻接矩阵表示的无向图
typedef struct {int V; // 顶点数int E; // 边数int G[100][100]; // 邻接矩阵
} Graph;// 获取边的权重
int getWeight(Graph G, int u, int v) {return G[u][v];
}// Kruskal算法求最小生成树
Graph primMST(Graph G) {int V = G.V; // 顶点数int E = G.E; // 边数int parent[V]; // 父节点数组int dist[V]; // 从源点到每个顶点的距离数组int i, u, v;int minCost = 0; // 总代价int edgeCount = 0; // 已选边的数量Graph mstEdges; // MST边集合memset(parent, -1, sizeof(parent)); // 初始化父节点为-1memset(dist, INF, sizeof(dist)); // 初始化距离为正无穷大priorityQueueNode pq; // 优先队列头结点pq.data = (void*)&dist[0];pq.index = 0;memset(mstEdges.G, 0, sizeof(mstEdges.G)); // 将邻接矩阵清零mstEdges.V = V;mstEdges.E = 0;do { // 不断扩展最小生成树,直到不存在增广路为止u = pq.index; // 取出距离源点最近的顶点uif (u == -1) break; // 如果已经没有顶点可选了,跳出循环pq.index = parent[u]; // 将当前顶点更新为其父节点edgeCount++; // 已选边的数量加1for (i = 0; i < G.V; i++) { // 遍历所有顶点v = i; // 从当前顶点开始选择下一个顶点vif (dist[v] > dist[u] + G.G[u][v]) { // 如果从u到v的距离比从u到源点的距离更短,则更新距离和优先级队列头结点dist[v] = dist[u] + G.G[u][v];pq.data = (void*)&dist[0];pq.index = i;} else if (i != u && v != u) { // 如果当前顶点u不是目标顶点,且从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并更新优先级队列头结点的位置mstEdges.G[edgeCount] = getWeight(G, u, v);pq.data = (void*)&mstEdges.G[0];pq.index = edgeCount++;} else if (i == u && v != u) { // 如果当前顶点u是目标顶点,但从u到v的距离比从u到源点的距离更短,则将边的权重加入到最小生成树中,并将当前顶点更新为其父节点的值为已选边的数量减一(因为此时已经找到了一条增广路径)mstEdges.G[edgeCount] = getWeight(G, u, v);parent[v] = edgeCount--; // 将当前顶点的父节点设为已选边的数量减一(因为此时已经找到了一条增广路径)} else if (i != u && v == u) continue; // 如果当前顶点u是目标顶点且从u到v的距离等于从u到源点的距离,则不需要进行任何操作,直接跳过本次循环继续下一次循环迭代}minCost += mstEdges.G[mstEdges.E-1]; // 将总代价加上已选边的权重之和作为新的总代价mstEdges.E++; // 将已选边的计数加一,表示又选了一条边加入到最小生成树中} while (edgeCount < V); // 当已选边的数量小于顶点数时,继续扩展最小生成树直到不存在增广路为止
这个
#include <stdio.h>
#include <stdlib.h>
#define VertexMax 20 //最大顶点数为20typedef char VertexType; typedef struct
{VertexType begin;VertexType end;int weight;
}Edge;//边集数组edge[]的单元 typedef struct
{VertexType Vertex[VertexMax];//顶点数组 Edge edge[VertexMax];//边集数组 int vexnum;//顶点数 int edgenum;//边数
}EdgeGraph;void CreateEdgeGraph(EdgeGraph *E)
{int i;printf("请输入顶点数和边数:\n");printf("顶点数 n="); scanf("%d",&E->vexnum);printf("边 数 e="); scanf("%d",&E->edgenum);printf("\n"); //printf("\n"); printf("输入顶点(无需空格隔开):"); scanf("%s",E->Vertex);printf("\n\n");printf("输入边信息和权值(如:AB,15):\n");for(i=0;i<E->edgenum;i++){printf("请输入第%d边的信息:",i+1);scanf(" %c%c,%d",&E->edge[i].begin,&E->edge[i].end,&E->edge[i].weight);}
}void print(EdgeGraph *E)
{int i;printf("\n-----------------------------------\n"); printf(" 顶点数组Vertex:");for(i=0;i<E->vexnum;i++){printf("%c ",E->Vertex[i]);}printf("\n\n");printf(" 边集数组edge:\n\n");printf("\t\tBegin End Weight\n");for(i=0;i<E->edgenum;i++){printf("\tedge[%d] %c %c %d\n",i,E->edge[i].begin,E->edge[i].end,E->edge[i].weight);}printf("\n-----------------------------------\n");
}void sort(EdgeGraph *E)
{int i,j;Edge temp;for(i=0;i<E->edgenum-1;i++){for(j=i+1;j<E->edgenum;j++){if(E->edge[i].weight>E->edge[j].weight){temp=E->edge[i];E->edge[i]=E->edge[j];E->edge[j]=temp;}}}
}int LocateVex(EdgeGraph *E,VertexType v)//查找元素v在一维数组 Vertex[] 中的下标,并返回下标
{int i;for(i=0;i<E->vexnum;i++){if(v==E->Vertex[i]){return i; } } printf("No Such Vertex!\n");return -1;
}int FindRoot(int t,int parent[])//t接收到是结点在Vertex数组中的下标
{while(parent[t]>-1)//parent=-1表示没有双亲,即没有根节点 {t=parent[t];//逐代查找根节点 }return t;//将找到的根节点返回,若没有根节点返回自身
}void MiniSpanTree_Kruskal(EdgeGraph *E)
{int i;int num;//生成边计数器,当num=顶点数-1 就代表最小生成树生成完毕 int root1,root2; int LocVex1,LocVex2; int parent[VertexMax];//用于查找顶点的双亲,判断两个顶点间是否有共同的双亲,来判断生成树是否会成环 //1.按权值从小到大排序 sort(E);print(E);//2.初始化parent数组 for(i=0;i<E->vexnum;i++){parent[i]=-1;}printf("\n 最小生成树(Kruskal):\n\n");//3.for(num=0,i=0;i<E->edgenum;i++){LocVex1=LocateVex(E,E->edge[i].begin);LocVex2=LocateVex(E,E->edge[i].end);root1=FindRoot(LocVex1,parent);root2=FindRoot(LocVex2,parent);if(root1!=root2)//若不会成环,则在最小生成树中构建此边 {printf("\t\t%c-%c w=%d\n",E->edge[i].begin,E->edge[i].end,E->edge[i].weight);//输出此边 parent[root2]=root1;//合并生成树num++;if(num==E->vexnum-1)//若num=顶点数-1,代表树生成完毕,提前返回 {return;}} }}int main()
{EdgeGraph E;CreateEdgeGraph(&E);MiniSpanTree_Kruskal(&E);return 0;}
.
代码的初始值为-1,思路都一样
五、最短路径
(一)迪杰斯特拉算法
可以算出开始点到其他点的最短距离
顶点i | 第一轮 | 第二轮 | 第三轮 | 第四轮 | 第五轮 | 第六轮 |
---|---|---|---|---|---|---|
1 | 6 V0->V1 | 5 V0->V2->V1 | ||||
2 | 3 V0->V2 | |||||
3 | 8 V0->V2->V3 | 6 V0->V2->V1->3 | ||||
4 | 9 V0->V2->V1->V4 | 9 V0->V2->V1->V4 | ||||
5 | 10 V0->V2->V5 | 10 V0->V2->V5 | 10 V0->V2->V5 | 10 V0->V2->V5 | ||
6 | 11 V0->V2->V1->V4->v6 | 11 V0->V2->V1->V4->v6 | ||||
S | {0,2} | {0,2,1} | {0,2,1,3} | {0,2,1,3,4} | {0,2,1,3,4,5} | {0,2,1,3,4,5,6} |
代码
#include<stdio.h>
#include<stdlib.h>
//最短路径算法 迪杰斯特拉 求有向图G 从某一个顶点开始 到其余各个顶点的最短路径P以及带权长度
//P为前驱顶点的下标 D为从选取的顶点V0到V顶点的最短路径长度typedef int P[200];//储存最短路径下标
typedef int D[200];//v0到v的最短路径长度和//邻接矩阵
typedef struct AdjacentMatrix
{//顶点集int Vertices[200];//边集int Edge[200][200];//顶点数 边数int numV, numE;
}AdjMatrix;void creategrahp(AdjMatrix* G)
{int n, e;//n代表顶点数 e代表边数int vi, vj;//vi vj代表边的两个顶点对int w;//表示边的权值printf("要输入的顶点数和边数\n");scanf("%d%d",&n,&e);G->numV = n; G->numE = e;//图的初始化for(int i = 0; i < n; i++){for(int j = 0; j < n; j++){if(i == j){//一个非带权的图 0代表没有边 1代表有边//边不指向自己 即对角线为0G->Edge[i][j] = 0;}else{//如果是带权的图 初始化为0或者为一个不可能的值G->Edge[i][j] = 65535;}}}//将顶点存入数组for(int i = 0; i < G->numV; i++){printf("请输入第%d个节点的信息\n",i + 1);scanf("%d", &G->Vertices[i]);}//输入边的信息for(int i = 0; i< G->numE; i++){//如果输入的是顶点的值 需要从顶点集中查找对应的下标 //如果是带权图 还要输入权的信息printf("请输入边的信息Vi,Vj和权值w\n");scanf("%d%d%d",&vi,&vj,&w);G->Edge[vi][vj] = w;//如果是带权图 等于权值//如果是有向图 就不对称//如果是无向图 矩阵对称G->Edge[vj][vi] = w;}
}void ShortPath_Dijkstra(AdjMatrix* G, int v0, P* p, D* d)
{int k;//记录当前最短路径的下标int final[200];//final[x] = 1 表示已求得的到v0的最短路径//初始化DPFinalfor(int i = 0; i < G->numV; i++){final[i] = 0;//初始化为未知状态(*d)[i] = G->Edge[v0][i];//如果v0传进的是值 寻找下标(*p)[i] = -1;}final[v0] = 1;(*d)[v0] = 0;//自己到自己的路径为0//主循环 求每次v0到v的最短路径for(int i = 1; i < G->numV; i++){int min = 65535;//寻找与v0距离最近的顶点for(int j = 0; j < G->numV; j++){if(final[j] != 1 && (*d)[j] < min){min = (*d)[j];k = j;}}//把Vk加入到最短路径中final[k] = 1;//修正当前最短路径的距离//以Vk作为中转,更新以Vk为中心的邻接点到V0的距离for(int j = 0; j < G->numV; j++){//如果当前找到v的顶点的路径小于原来的路径长度if(min + G->Edge[k][j] < (*d)[j] && final[j] != 1){//说明找到了更短的路径 修改DP(*d)[j] = min + G->Edge[k][j];(*p)[j] = k;}}}
}int main()
{AdjMatrix* G = (AdjMatrix*)malloc(sizeof(AdjMatrix));int p[200];int d[200];creategrahp(G);int v0 = 0;ShortPath_Dijkstra(G, v0, &p, &d);for (int i = 1; i < G->numV; i++){printf("v%d - v%d:", v0, i);int j = i;while (p[j] != -1){printf("%d", p[j]);j = p[j];} printf("\n");}printf("最短路径长度");for (int i = 1; i < G->numV; i++){printf("v%d-v%d : %d\n", G->Vertices[0], G->Vertices[i], d[i]);}system("pause");return 0;
}
这里的final[]相当于S集合
时间复杂度O()
(二)弗洛伊德算法
这个算的是所有点之间的最短长度
代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdbool.h>
#include <malloc.h>
#define MVNum 100//最大顶点数
#define MaxInt 66666//表示极大值
typedef struct {char vexs[MVNum];//顶点表(顶点为字符型)int arcs[MVNum][MVNum];//邻接矩阵(权值为整型)int vexnum, arcnum;//图的当前点数和边数
}AMGraph;//定位
int LocateVex(AMGraph* G, char v) {int i;for (i = 0; i < G->vexnum; i++) {if (G->vexs[i] == v) {return i;}}return -1;
}//无向网的建立
AMGraph* CreateUDN() {int i, j, k, w;char v1, v2;AMGraph* G = malloc(sizeof(AMGraph));printf("输入总顶点数,边数\n");scanf("%d%d", &G->vexnum, &G->arcnum);getchar();//吸收换行符printf("依次输入点的信息\n");for (i = 0; i < G->vexnum; i++) {scanf("%c", &G->vexs[i]);}getchar();//吸收换行符for (i = 0; i < G->vexnum; i++)for (j = 0; j < G->vexnum; j++) {if (i == j) {G->arcs[i][j] = 0;}else {G->arcs[i][j] = MaxInt;}}for (k = 0; k < G->arcnum; k++) {printf("输入一条边依附的顶点及权值\n");scanf("%c%c", &v1, &v2);scanf("%d", &w);getchar();//吸收换行符i = LocateVex(G, v1), j = LocateVex(G, v2);//确定v1、v2在顶点数组的下标G->arcs[i][j] = w;//边<v1,v2>权值置为wG->arcs[j][i] = w;//无向网对称边<v2,v2>权值也置为w}return G;
}//输出邻接矩阵
void print(AMGraph* G) {int i, j;printf(" ");for (i = 0; i < G->vexnum; i++) {printf("%c ", G->vexs[i]);}printf("\n");for (i = 0; i < G->vexnum; i++) {printf("%c ", G->vexs[i]);for (j = 0; j < G->vexnum; j++) {if (G->arcs[i][j] == MaxInt)printf("∞ ");elseprintf("%d ", G->arcs[i][j]);}printf("\n");}
}//弗洛伊德算法
void Floyd(AMGraph* G) {int distance[MVNum][MVNum];int i, j, k;//初始化距离矩阵for (i = 0; i < G->vexnum; i++) {for (j = 0; j < G->vexnum; j++) {distance[i][j] = G->arcs[i][j];}}//中间节点迭代for (k = 0; k < G->vexnum; k++) {for (i = 0; i < G->vexnum; i++) {for (j = 0; j < G->vexnum; j++) {if (distance[i][k] + distance[k][j] < distance[i][j]) {distance[i][j] = distance[i][k] + distance[k][j];}}}}//输出printf("每对顶点间的最短距离:\n");for (i = 0; i < G->vexnum; i++) {for (j = 0; j < G->vexnum; j++) {if (distance[i][j] == MaxInt) {printf("∞ ");}else {printf("%d ", distance[i][j]);}}printf("\n");}
}int main() {AMGraph* G = CreateUDN();printf("该无向网邻接矩阵为:\n");print(G);Floyd(G);
}
关键判断distance[i][k] + distance[k][j] < distance[i][j]关系
加上path
void ShortestPath_Floyd(Graph G, int** Path, int** D) //Path,D 为二维数组
{int v, w, k;for (v = 0; v < G.numv; v++){for (w = 0; w < G.numv; w++){D[v][w] = G.edge[v][w]; //初始化 D 数组为邻接矩阵//若 v 与 w 之间有弧,将 w 的前驱置为 v//若 v 与 w 之间没有弧,将 w 的前驱置为 -1if (D[v][w] < INFINITY){path[v][w] = v;}else{path[v][w] = -1;}}}for (k = 0; k < G.numv;k++){for (v = 0; v < G.numv; v++){for(w = 0;w < G.numv; w++){if (D[v][w] > D[v][k] + D[k][w]) //如果从 v 经 k 到 w 的路径最短{D[v][w] = D[v][k] + D[k][w];Path[v][w] = Path[v][k];}}}}//显示最短路径printf("各顶点间最短路径如下:\n"); for(v = 0; v < G.numv; v++) { for(w = v+1; w < G.numv; w++) {printf("v%d-v%d weight: %d ",v,w,D[v][w]);k = Path[v][w]; //获得第一个路径的顶点下标printf(" path: %d",v); //打印源点 while(k != w) //如果路径顶点下标不是终点 {printf(" -> %d",k); //打印路径顶点 k = Path[k][w]; //获得下一个路径顶点下标}printf(" -> %d\n",w); //打印终点}printf("\n");}
}
六、拓扑排序(这个不考)
有向无环图
(一)AOV网
- 若是从i到j有一条有向路径,则i是j的前驱,j是i的后继
- 若<i,j> 是网中有向边,则i是j的直接前驱,j是i的直接后继
- AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然不可能
(二)拓扑排序
1、在有向图中选一个没有前驱的顶点且输出
2、从图中删除该顶点和所有以他为尾的弧
3、重复上述步骤,直到所有的顶点均已输出,或者当图中不存在无前驱的顶点为止
比如我们要对上述图进行拓扑排序 第一次寻找的是A这一个结点 因为A没有前驱 接下来就是删除A以及以A为弧尾的弧,此时就是如下图
在从中选择一个没有前驱的结点,这里BCD 都可以,这里我们就选择B 同样的道理 删除B以及以B为弧尾的弧此时就是如下图
同样的道理 再选一个C删除以C为弧尾的弧
再删除 D
再删除E
所有总的拓扑排序是 当然排序可能不止一种 因为可能不止一个结点在某一步骤中是无前驱的
ABCDE 有人可能会说了这个图太简单了,能不能难一点的图来拓扑
如下
拓扑排序可以如下
AEBDC
EABDC
AEDBC
EADBC
(三)判断一个有向图是否为有向无环图的方法是?
方法一∶深度优先遍历
若从有向图上的某个顶点u出发,在DFS(u)结束之前出现一条从顶点v到u的边,由于v在生成树上是u的子孙,则图中必定存在包含u和v的环,因此深度优先遍历可以检测一个有向图是否有环。
方法二∶拓扑排序
数组
//有向无环图的拓扑排序
#include<stdio.h>
#include<stdlib.h>
typedef struct graph
{char* vexs;//顶点数值int** arcs;//邻接矩阵int vexNum;//顶点数int arcNum;//边数
}Graph;typedef struct Node//栈的建立与存顶点下标有关
{int data;struct Node* next;
}Node;Node* initStack()
{Node* stack=(Node*)malloc(sizeof(Node));stack->data=0;stack->next=NULL;return stack;
}void push(Node* stack,int data)//压栈
{Node* node=(Node*)malloc(sizeof(Node));node->data=data;node->next=stack->next;stack->next=node;stack->data++;
}int isEmpty(Node* stack)//判断是否为空栈
{if(stack->next==NULL){return 1;}else{return 0;}
}int pop(Node* stack)//出栈
{if(isEmpty(stack)){return -1;}else{Node* node=stack->next; int data=node->data;stack->next=node->next;free(node);stack->data--;return data;}
}Graph* initGraph(int vexNum)//分配空间
{Graph* G = (Graph*)malloc(sizeof(Graph));G -> vexs = (char*)malloc(sizeof(char) * vexNum);G -> arcs = (int**)malloc(sizeof(int*) * vexNum);for (int i = 0 ; i < vexNum; i++) {G -> arcs[i] = (int*)malloc(sizeof(int) * vexNum);}G -> vexNum = vexNum;G -> arcNum = 0;return G;
}void createGraph(Graph* G, char* vexs, int* arcs)//创建图
{for (int i = 0 ; i < G -> vexNum; i++) {G -> vexs[i] = vexs[i];for (int j = 0; j < G -> vexNum; j++) {G -> arcs[i][j] = *(arcs + i * G -> vexNum + j);if (G -> arcs[i][j] != 0)G -> arcNum ++;}}G -> arcNum /= 2;
}int* findInDegrees(Graph* G)//找出入度
{int* inDegrees=(int*)malloc(sizeof(int)*G->vexNum);for(int i=0;i<G->vexNum;i++)//初始化{inDegrees[i]=0;}for(int i=0;i<G->vexNum;i++){for(int j=0;j<G->vexNum;j++){if(G->arcs[i][j]){inDegrees[j]++;}}}return inDegrees;
}void topologicalSort(Graph* G)//拓扑排序
{int index=0;int* top=(int*)malloc(sizeof(int)*G->vexNum);//存下标的数组int* inDegrees=findInDegrees(G);Node* stack=initStack();for(int i=0;i<G->vexNum;i++)//入度为0的压栈{if(inDegrees[i]==0){push(stack,i);}}while(!isEmpty(stack))//栈不为空,循环执行入度的减法(去掉输出顶点指向的下一个顶点的边){int vexindex=pop(stack);//出栈的是顶点的下标top[index++]=vexindex;//保存顶点下标for(int j=0;j<G->vexNum;j++){if(G->arcs[vexindex][j])//下一个顶点有入度时减去{inDegrees[j]--;if(inDegrees[j]==0)//顶点入度减到0了直接入栈{push(stack,j);}}}}for(int i=0;i<G->vexNum;i++)//依次输出入度为零的顶点{printf("%c ",G->vexs[top[i]]);}printf("\n");
}void DFS(Graph* G,int* flag,int index)//深度优先遍历
{printf("%c ",G->vexs[index]);flag[index]=1;//已经访问过顶点标记为1,之后不会再访问for(int i=0;i<G->vexNum;i++){if(G->arcs[index][i]==1&&!flag[i]){DFS(G,flag,i);}}
}int main()
{Graph* G=initGraph(6);int* flag=(int*)malloc(sizeof(int)*G->vexNum);for(int i=0;i<G->vexNum;i++)//首先赋值为0,表示未访问任何顶点{flag[i]=0;}int arcs[6][6]={0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,1,0};createGraph(G,"123456",(int*)arcs);DFS(G,flag,0);printf("\n");topologicalSort(G);return 0;
}
邻接表
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 15
#define MAXINT 10000
int get_index(int* arr, int e)//得到输出的顶点,在数组中的下标
{int i = 0;for(i = 0; i < MAXSIZE; i++){if(arr[i] == e)return i;}return -1;
}void TopologicalSorting(int arcs[][MAXSIZE])
{int m,n,i,j,a,b;int vertex[MAXSIZE] = {0};//初始化顶点数组int visited[MAXSIZE] = {0};printf("请输入顶点数和弧的数目:>");scanf("%d%d",&m,&n);for(i = 0; i < m; i++){printf("请输入每一个顶点:>");getchar();scanf("%d", &vertex[i]);}for(j = 0; j < n; j++){printf("请输入每一条边:>");getchar();scanf("%d%d",&a,&b);arcs[get_index(vertex,a)][get_index(vertex,b)] = 1;//根据输入更新邻接矩阵}int num = 0;while(num != m)//找到全部顶点就结束{for(i = 0; i < m; i++)//循环遍历邻接矩阵{for(j = 0; j < m; j++){//如果找到某一列的数据全部为0,说明这一列所代表的顶点是没有前驱的//这里visited[j],是用来限制某一行,visited[i]为限制某一列if((arcs[j][i] == 1 && !visited[j]) || visited[i])break;}if(j == m){printf("%d",vertex[i]);//输出没有前驱的顶点num++;//找到的顶点数加1visited[i] = 1;//设置这个顶点为已访问}}}}int main()
{int arcs[MAXSIZE][MAXSIZE]={{0}};//邻接矩阵TopologicalSorting(arcs);return 0;
}
栈的实现
#include <stdio.h>
#include <stdlib.h>#define MAX_VER_NUM 2000
#define VertexData int
#define ERROR 0
#define OK 1typedef enum { DG, UDG, DN, UDN }GraphKind;typedef struct ArcNode{VertexData adjvex;struct ArcNode* nextarc;
}ArcNode;typedef struct VexNode {VertexData data;ArcNode* firstArc;
}VertexNode;typedef struct {VertexNode vertex[MAX_VER_NUM];int num_vex, num_arc;GraphKind kind;
}AdjList;typedef struct {int data[MAX_VER_NUM];int top;
}Stack;Stack InitStack(); //初始化栈
void PushStack(Stack* S, int v); //入栈
void PopStack(Stack* S, int* v); //出栈
int LocateVertex(AdjList A, VertexData v);//找到顶点位置
AdjList* CreateGraph(int n, int m); //创建图
void FindID(AdjList A, int indegree[MAX_VER_NUM]);//求入度
int TopoSort(AdjList A); //拓扑排序int main()
{int n, m;scanf("%d%d", &n, &m);AdjList *A;A = CreateGraph(n, m);TopoSort(*A);return 0;
}Stack InitStack()
{ //初始化栈Stack S;S.top = -1;return S;
}void PushStack(Stack *S, int v)
{ //入栈if (S->top == MAX_VER_NUM - 1) {return;}S->top++;S->data[S->top] = v;
}void PopStack(Stack* S, int *v)
{ //出栈if (S->top == -1) {return;}*v = S->data[S->top];S->top--;
}int LocateVertex(AdjList A, VertexData v)
{ //找到顶点位置for (int i = 0; i < A.num_vex; i++) {if (A.vertex[i].data == v) {return i;}}return -1;
}AdjList* CreateGraph(int n, int m)
{ //创建图AdjList *A;A = (AdjList*)malloc(sizeof(AdjList));if (A == NULL) {return NULL;}A->kind = DG;A->num_vex = n;A->num_arc = m;for (int i = 0; i < A->num_vex; i++) {scanf("%d", &A->vertex[i].data);//若要输入字符则要改变A->vertex[i].firstArc = NULL;}VertexData v1, v2;int loc1;for (int i = 0; i < A->num_arc; i++) {scanf("%d%d", &v1, &v2);//若要输入字符则要改变ArcNode* tmp;tmp = (ArcNode*)malloc(sizeof(ArcNode));if (tmp == NULL) {return 0;}tmp->adjvex = v2;loc1 = LocateVertex(*A, v1);tmp->nextarc = A->vertex[loc1].firstArc;A->vertex[loc1].firstArc = tmp;}return A;
}void FindID(AdjList A, int indegree[MAX_VER_NUM])
{ //求入度ArcNode* tmp;tmp = (ArcNode*)malloc(sizeof(ArcNode));if (tmp == NULL) {return;}for (int i = 0; i < A.num_vex; i++) {indegree[i] = 0;}int loc;for (int i = 0; i < A.num_vex; i++) {tmp = A.vertex[i].firstArc;while (tmp != NULL) {loc = LocateVertex(A, tmp->adjvex);indegree[loc]++;tmp = tmp->nextarc;}}
}int TopoSort(AdjList A)
{ //拓扑排序Stack S;int indegree[MAX_VER_NUM];int count = 0;//记录遍历顶点数,若小于A.num_vex,则A中有环ArcNode* tmp;tmp = (ArcNode*)malloc(sizeof(ArcNode));if (tmp == NULL) {return 0;}FindID(A, indegree);S = InitStack();for (int i = 0; i < A.num_vex; i++) {if (indegree[i] == 0) {PushStack(&S, i);}}int v, k, loc_k;while (S.top != -1) {PopStack(&S, &v);printf("%d ", A.vertex[v].data);//若要输入字符则要改变count++;tmp = A.vertex[v].firstArc;while (tmp != NULL) {k = tmp->adjvex;loc_k = LocateVertex(A, k);indegree[loc_k]--;if (indegree[loc_k] == 0) {PushStack(&S, loc_k);}tmp = tmp->nextarc;}}if (count < A.num_vex) {return ERROR;}else return OK;
}
队列
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define MAX_VERTEX_NUM 20 // 最大顶点数typedef struct {char vertex[MAX_VERTEX_NUM]; // 存放顶点信息int edge[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 存放边信息int vertex_num; // 顶点数int edge_num; // 边数
} Graph;int indegree[MAX_VERTEX_NUM]; //存放每个顶点的入度
/*这里我们不要判断某个顶点的出度,在拓扑排序中我们用不到这些*///初始化顶点的入度数组
void Initindegree(Graph G){//首先初始化入度数组for(int i=0;i<G.vertex_num;i++){indegree[i]=0;}//统计每个元素对应邻接矩阵列中1的个数即可判断有几条边进入for(int i=0;i<G.vertex_num;i++){int count=0;for(int j=0;j<G.vertex_num;j++){if(G.edge[j][i]==1)count++;}indegree[i]=count;}
}// 拓扑排序函数
bool Topologicalsort(Graph G) {char queue[MAX_VERTEX_NUM];int front = -1, rear = -1; // 初始化队列,front和rear都设为0// 首先把入度为0的顶点都进队列for (int i = 0; i < G.vertex_num; i++) {if (indegree[i] == 0) {queue[++rear] = i; // 注意这里应该存储顶点的索引,而不是顶点的值}}int count = 0; // 这个变量用来记录当前已输出的顶点数while (front != rear) {int cur_vertex = queue[++front]; // 出队一个顶点,front原先是零,现在是1printf("%c ", G.vertex[cur_vertex]); // 输出顶点的值count++; // 已输出的顶点数加1// 遍历以当前顶点为起点的所有边for (int i = 0; i < G.vertex_num; i++) {if (G.edge[cur_vertex][i] == 1) {indegree[i]--; // 将这些边的终点的入度减1if (indegree[i] == 0) { // 如果某个终点的入度变为0,就将其入队queue[++rear] = i;}}}}if (count == G.vertex_num) { // 如果已输出的顶点数等于总顶点数,说明拓扑排序成功return true;} else { // 否则说明存在环,拓扑排序失败return false;}
}int main(){Graph G;G.vertex_num=5;G.edge_num=6;//人为构造图的顶点for(int i=0;i<G.vertex_num;i++){G.vertex[i]= 'A'+i;}for(int i=0; i<G.vertex_num; i++){for(int j=0; j<G.vertex_num; j++){G.edge[i][j]=0;}}//人为构造图的边G.edge[0][1]=1;G.edge[0][3]=1;G.edge[1][3]=1;G.edge[1][2]=1;G.edge[3][4]=1;G.edge[2][4]=1;G.edge[3][2]=1;Initindegree(G);Topologicalsort(G);
}
七、关键路径
1、AOE网
2、AOE网的性质
(1)只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
(2)只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生,另外有些活动是可以并行进行的
3.关键路径
从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长。
4.最早最晚时间
(1)事件vk的最早发生时间ve(k):决定了所有从vk开始的活动能够开工的最早时间;
(2)活动ai的最早开始时间e(i):指该活动弧的起点所表示的事件的最早发生时间;
(3)事件vk的最迟发生时间vl(k):指在不推迟整个工程完成的前提下,该事件最迟必须发生的时间;
(4)活动ai的最迟开始时间l(i):指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差;
(5)活动ai的时间余量:d(i)=l(i)-e(i),表示在不增加完成整个工程所需总时间的情况下,活动ai可以拖延的时间;
(6)若一个活动的时间余量为0,则说明该活动必须要如期完成,d(i)=0即l(i)=e(i)的活动ai是关键活动,由关键活动组成的路径就是关键路径。
5、求关键路径
1.步骤
- (1)求所有事件的最早发生时间ve();
- (2)求所有事件的最迟发生时间vl();
- (3)求所有活动的最早发生事件e();
- (4)求所有活动的最迟发生时间l();
- (5)求所有活动的时间余量d();
- (6)d(i)=0的就是关键活动,所有关键活动组成的路径即为关键路径。
2.举例
1.定义一维数组ve[i]并置初值0(记录事件的最早发生时间)、一维数组vI[i](记录事件的最晚发生时间)、一维数组topo[i](记录拓扑序列的顶点序号)。
2.调用拓扑排序算法,使拓扑序列保存在topo中。
3.根据topo中的值,按从前向后的拓扑次序,依次求每个事件的最早发生时间,循环n(总顶点数)次,执行以下操作:
取得拓扑序列中的顶点序号k,k= topo[i];
用指针p依次指向k的每个邻接顶点,取得每个邻接顶点的序号j,j=p->adjvex,依次更新顶点j的最早发生时间ve[j](if(ve[i]<ve[k]+ p->weight) ve[i]= ve[k] + p->weight);
4.将每个事件的最迟发生时间vl[i]初始化为汇点的最早发生时间,即vl[i]=ve[n-1]。
5.根据topo中的值,按从后向前的逆拓扑次序,依次求每个事件的最迟发生时间,循环n次,执行以下操作:取得拓扑序列中的顶点序号k,k= topo[i];
用指针p依次指向k的每个邻接顶点,取得每个邻接顶点的序号j,j=p->adjvex,依次根据 k的邻接点,更新k的最迟发生时间vI[k](if(vl[k]>vl[j]-p->weight) vl[k]=vl[j]-p->weight);
6.判断某一活动是否为关键活动,循环n次,执行以下操作:对于每个顶点vi,用指针p依次指向vi的每个邻接顶点,取得每个邻接顶点的序号j,j=p->adjvex,分别计算活动<vi,vj>的最早和最晚开始时间e和l;
如果e和l相等,则活动<vi,vj>为关键活动,输出弧<vi,vj>。
代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>#define MVNUM 100typedef struct ArcNode {int adjvex;int weight;struct ArcNode* next;
}ArcNode;typedef struct VNode {char data;ArcNode* firstarc;
}VNode, AdjList[MVNUM];typedef struct {AdjList vertices;int vexnum, arcnum;
}ALGraph;
//定位
int LocateVex(ALGraph* G, char v) {int i;for (i = 0; i < G->vexnum; i++) {if (G->vertices[i].data == v) {return i;}}return -1;
}//有向网邻接表的建立
ALGraph* CreateGraph() {int i, j, k, v1, v2, w;ALGraph* G = malloc(sizeof(ALGraph));printf("输入顶点数和边数:\n");scanf("%d%d", &G->vexnum, &G->arcnum);getchar();printf("输入顶点信息:\n");for (i = 0; i < G->vexnum; i++) {scanf("%c", &G->vertices[i].data);G->vertices[i].firstarc = NULL;}getchar();for (k = 0; k < G->arcnum; k++) {printf("输入一条弧依附的起点和终点和权重:\n");scanf("%c%c%d", &v1, &v2, &w);getchar();i = LocateVex(G, v1), j = LocateVex(G, v2);ArcNode* p = malloc(sizeof(ArcNode));p->adjvex = j;p->weight = w;p->next = G->vertices[i].firstarc;G->vertices[i].firstarc = p;}return G;
}//---------------------------------------------------------------------------
//拓扑排序
bool TopoSort(ALGraph* G, int* topo) {int indegree[MVNUM] = { 0 };//记录顶点入度int i, j, v;int count = 0;//记录输出的顶点数int stack[MVNUM];//定义一个简单的栈int top = 0;//栈顶指针ArcNode* p = NULL;//统计各顶点入度for (i = 0; i < G->vexnum; i++) {p = G->vertices[i].firstarc;while (p != NULL) {indegree[p->adjvex]++;p = p->next;}}//将所有入度为0的顶点入队for (i = 0; i < G->vexnum; i++) {if (indegree[i] == 0) {stack[top++] = i;}}//栈非空时循环while (top > 0) {v = stack[--top];//出栈一个顶点topo[count++] = v;//排序的顶点数加1并将顶点存入数组topo//遍历该顶点为起点的所有边p = G->vertices[v].firstarc;while (p != NULL) {indegree[p->adjvex]--;//入度减1//若入度为0,入队if (indegree[p->adjvex] == 0) {stack[top++] = p->adjvex;}p = p->next;//指向下一条边}}//输出顶点数小于总顶点数,说明有环if (count < G->vexnum) {printf("该图有环,拓扑排序失败。\n");return false;}return true;
}
//----------------------------------------------------------------------------
//关键路径算法
bool CriticalPath(ALGraph* G) {int topo[MVNUM];//储存拓扑序列int ve[MVNUM] = { 0 };//记录每个事件最早发生时间int vl[MVNUM];//记录每个事件最晚发生时间int i, j, k, e, l;ArcNode* p = NULL;if (!TopoSort(G, topo)) return false;//若拓扑排序失败,则存在有向环,返回false//按拓扑次序求每个事件的最早发生时间for (i = 0; i < G->vexnum; i++) {k = topo[i];//取拓扑序列中的顶点序号kp = G->vertices[k].firstarc;//指针p指向顶点k的第一个邻接点//更新顶点k的所以邻接点的最早发生时间while (p != NULL) {j = p->adjvex;//j为顶点k的邻接点的序号//更新顶点j最早发生时间(不理解的话,找个AOE-网代入联想下)if (ve[j] < ve[k] + p->weight) {ve[j] = ve[k] + p->weight;}p = p->next;//指针噗指向下一邻接点}}//初始化数组Vlfor (i = 0; i < G->vexnum; i++) {vl[i] = ve[G->vexnum - 1];}//按逆拓扑次序求每个事件的最晚发生时间for (i = G->vexnum - 1; i >= 0; i--) {k = topo[i];//取拓扑序列中的顶点序号kp = G->vertices[k].firstarc;//指针噗指向顶点k的第一个邻接点//根据顶点k的邻接点更新k的最晚发生时间while (p != NULL) {j = p->adjvex;//j为邻接点序号//更新顶点k的最晚发生时间if (vl[k] > vl[j] - p->weight) {vl[k] = vl[j] - p->weight;}p = p->next;//指针p指向下一邻接点}}//判断每一活动是否为关键活动//每次循环针对以顶点i为开始点关联的所有活动for (i = 0; i < G->vexnum; i++) {p = G->vertices[i].firstarc;//指针p指向顶点i的第一个邻接点while (p != NULL) {j = p->adjvex;//j为邻接点序号e = ve[i];//计算活动<i,j>的最早开始时间l = vl[j] - p->weight;//计算活动<i,j>的最晚开始时间//若为关键活动,则输出if (e == l) {printf("<%c,%c> ", G->vertices[i].data, G->vertices[j].data);}p = p->next;//指针p指向下一邻接点}}
}int main() {ALGraph* G = CreateGraph();printf("关键活动为:\n");CriticalPath(G);return 0;
}
总结
关键路径算法是一个解决项目管理中最短时间问题的经典工具。在计算关键路径时,我们需要对整个项目进行遍历,并计算出每个活动的最早开始时间、最晚开始时间、最早完成时间以及最晚完成时间,进而得到整个项目的关键路径。因此,关键路径算法的时间复杂度相对较高,但也有一些优化方法可以应用。
算法的时间复杂度分析:对于有 n 个顶点和 e 条弧的 AOE 网而言,拓扑排序的时间复杂度是 O(n+e),计算事件的最晚发生时间的时间复杂度也是 O(n+e),最后计算关键路径的时间复杂度还是 O(n+e),所以整体的时间复杂度依然是 O(n+e)。
结束结束,长脑子了!
相关文章:

数据结构7---图
一、定义 对于图的定义,我们需要明确几个注意的地方:一线性表中我们把数据元素叫元素,树中叫结点,在途中数据元素我们则称之为顶点(Vertex)。 对于图的定义,我们需要明确几个注意的地方: 线性表中我们把数据元素叫元素…...

Excel 如何复制单元格而不换行
1. 打开excle, sheet1右键单击>查看代码>插入>模块 输入代码 Sub CopyText() Updated by NirmalDim xAutoWrapper As ObjectSet xAutoWrapper New DataObject or GetObject("New:{1C3B4210-F441-11CE-B9EA-00AA006B1A69}")xAutoWrapper.SetText ActiveC…...

前端 CSS 经典:mix-blend-mode 属性
前言:这是一个混合属性,作用是将两个颜色混合生成一个新颜色。可以将视频和文字相融合,产生动态文字效果。 效果 实现代码 <!DOCTYPE html> <html lang"en"><head><meta charset"utf-8" />&l…...

OpenCV--滤波器(一)
低通滤波器 代码和笔记 代码和笔记 import cv2 import numpy as np""" 滤波器--用于图像处理的重要工具,它们可以根据图像中像素的邻域信息来修改像素值,以实现去噪、模糊、锐化、边缘检测等效果。低通滤波器(Low-pass Filte…...

MK的前端精华笔记
文章目录 MK的前端精华笔记第一阶段:前端基础入门1、(1)、(2)、 2、3、4、5、6、7、 第二阶段:组件化与移动WebAPP开发1、(1)、(2)、 2、3、4、5、6、7、 第三…...

低代码平台框架:开源选型、实践与应用深度解析
文章目录 1.1 低代码平台的重要性与应用背景2.1 表单建模2.2 流程设计2.3 报表(打印)可视化2.4 代码生成器2.5 系统管理2.6 前端UI开源选型3.1 如何选择合适的开源框架3.2 市场上的主要开源低代码平台对比3.3 开源项目的技术栈与优缺点分析 5.1 成功案例…...

深度学习500问——Chapter12:网络搭建及训练(3)
文章目录 12.3.5 Caffe有哪些接口 12.4 网络搭建有什么原则 12.4.1 新手原则 12.4.2 深度优先原则 12.4.3 卷积核size一般为奇数 12.4.4 卷积核不是越大越好 12.5 有哪些经典的网络模型值得我们去学习的 12.6 网络训练有哪些技巧 12.6.1 合适的数据集 12.6.2 合适的预…...

Android使用DevRing框架搭建数据库实体类以及使用
一、引用DevRing依赖 //导入DevRing依赖implementation com.ljy.ring:devring:1.1.8创建数据库表的依赖implementation org.greenrobot:greendao:3.2.2 // add libraryimplementation org.greenrobot:greendao-generator:3.0.0 二、修改工程目录下的.idea->gradle.xml文件&…...

高效BUG管理:定级、分类和处理流程
高效BUG管理:定级、状态跟踪与处理全流程 前言一、BUG的定义二、BUG的定级三、BUG的状态四、BUG的处理流程1. BUG报告2. BUG确认3. BUG修复4. BUG验证5. BUG关闭 五、常见问题与解决方案六、总结 前言 在测试工作中,BUG的定级和分类是一个重要环节&…...

服务器数据恢复—raid5热备盘同步失败导致阵列崩溃如何恢复数据?
服务器存储数据恢复环境&故障: 某品牌DS5300存储,包含一个存储机头和多个磁盘柜,组建了多组RAID5磁盘阵列。 某个磁盘柜中的一组RAID5阵列由15块数据盘和1块热备硬盘组建。该磁盘柜中的某块硬盘离线,热备盘自动替换并开始同步…...

Ubuntu iso 镜像下载 步骤截图说明
Ubuntu镜像下载,在这个网址: Enterprise Open Source and Linux | Ubuntu 步骤如下图所示: 1、登入网址 2、点击Get Ubuntu 3、点击Download Ubuntu Desktop 后续点击Downloadload 24.04 LTS直接下载就行 如果需要下载其它版本…...

git拉取gitee项目到本地
git安装等不做赘述。 根据需要选择不同操作 1.只是单纯拉取个项目,没有后续的追踪等操作 不需要使用git init初始化本地文件夹 新建一个文件夹用于存储项目,右键选择 git bash here 会出现命令行窗口 如果像我一样,只是拉取个项目作业&…...

力扣42.接雨水
力扣42.接雨水 前后缀数组 对于每个一个位置 求其前面最高高度pre_max[i] max(pre_max[i-1] , h[i])和后面最高高度suf_max[i] max(suf_max[i1] , h[i])当前i处的水容量 为min(pre_max[i] , suf_max[i]) - h[i] class Solution {public:int trap(vector<int>& …...

国产数据库与MYSQL兼容性?开发应该怎么选择?
国产数据库主要包括以下几种: TiDB:由 PingCAP 公司研发设计的开源分布式 HTAP (Hybrid Transactional and Analytical Processing) 数据库,兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用等特性。 华为GaussDB…...

Spring框架中Bean的生命周期
Bean的生命周期通常指的是从创建到初始化,经过一系列的流程,最终销毁的过程。只不过,在Spring框架中,Bean的生命周期是由Spring IOC容器来管理的。在Spring中,我们定义Bean时,也可以自己指定初始化和销毁的…...

从零到一学FFmpeg:avformat_alloc_output_context2 函数详析与实战
文章目录 前言一、函数原型二、功能描述三、使用场景四、AVFormatContext 结构体五、代码实例 前言 avformat_alloc_output_context2 是FFmpeg库中的一个函数,用于为输出多媒体文件初始化一个AVFormatContext结构体。这个函数在开始输出音频、视频数据到文件之前被…...

Lua 绕过元表
Lua 绕过元表,直接访问 table 的字段。 绕过元表 rawset(table, index, value),在不触发元方法的情况下,设置 table[index] 的值为 value。 rawget(table, index),在不触发元方法的情况下,获取 table[index] 的值。…...

pip方法总结(极简快速掌握)
pip是Python的包管理工具,它允许用户从PyPI等源安装和管理额外的库和依赖。以下是关于pip使用方法的详细总结,同时附上代码演示: 一、pip的基本功能 安装包:使用pip install 包名命令可以安装指定的Python包。例如,要…...

aigc基础概念(一)
目录 一、AI 1.1、基本术语 1、Artificial Intelligence (AI) —— 人工智能 2、Generative AI —— 生成性人工智能 3、Machine Learning (ML) —— 机器学习 4、Deep Learning (DL) —— 深度学习 5、Large Language Model (LLM) —— 大型语言模型 6、Transformers …...

USB学习——12、usb初始化和插拔驱动软件流程大致框架描述
usb初始化和插拔驱动软件流程大致框架描述: 当设备启动时,usb的主机控制器设备驱动(HCD)和 usb的root hub会先初始化: 1、xhci-plat.c主机控制器驱动那里,__usb_creat_hcd创建usb主机数据结构,m…...

【ARMv8/ARMv9 硬件加速系列 2.4 -- ARM NEON Q寄存器与V寄存器的关系】
文章目录 Q 与 V 的关系向量寄存器 v 的使用赋值操作寄存器赋值总结Q 与 V 的关系 在ARMv8/v9架构中,v寄存器和q寄存器实际上是对相同的物理硬件资源的不同称呼,它们都是指向ARM的SIMD(单指令多数据)向量寄存器。这些寄存器用于高效执行向量和浮点运算,特别是在多媒体处理…...

Oracle中递归查询(START WITH……CONNECT BY……)
一、基本语法 在Oracle中START WITH……CONNECT BY……一般用来查找存在父子关系的数据,也就是树形结构的数据。 SELECT * FROM TABLE WHERE 条件3 START WITH 条件1 CONNECT BY 条件2;start with [condition]:设置起点,用来限制第一层的数…...

【云原生|K8S系列】如何创建Kubernetes job和Cronjobs 入门指南
本kubernetes教程解释了如何创建kubernetes作业和cronjobs,以及它的基础知识、用例和一些提示和技巧。 什么是Kubernetes Job? Kubernetes job和cronjob是Kubernetes对象,主要用于短期和批处理工作负载。 kubernetes作业对象基本上部署了一个pod&…...

力扣每日一题 6/23 字符串/模拟
博客主页:誓则盟约系列专栏:IT竞赛 专栏关注博主,后期持续更新系列文章如果有错误感谢请大家批评指出,及时修改感谢大家点赞👍收藏⭐评论✍ 520.检测大写字母【简单】 题目: 我们定义,在以下…...

Google trend搜索关键词
Google trend地址:https://trends.google.com/trends/?geoUS&hlzh-CN 1、具体的操作步骤如下: 2、Google trend搜索页面如下:...

Unity C#调用Android,IOS震动功能
最近在Unity上需要很原生移动端进行交互, 原理:新建一个android项目,把生成的app module给干掉,然后留下一个vibrationPlugin module,在这个module下写android震动代码,将这个android工程构建出来的 aar移…...

Ruby 注释
Ruby 注释 在编程中,注释是用于解释代码如何工作以及为什么这样编写的重要工具。Ruby作为一种解释型、面向对象的脚本语言,提供了灵活的注释方式,帮助开发者更好地组织和理解代码。本文将详细介绍Ruby中的注释类型、用法以及最佳实践。 Rub…...

C语言入门系列:特殊的main函数和exit函数
文章目录 一,main函数二,exit函数1,exit函数2,atexit()函数2.1 atexit函数的简介2.2 atexit注册的函数一定会被调用吗2.2.1 正常退出测试2.2.2 异常退出测试 一,main函数 一个C程序至少包含一个函数,这个函…...

JAVA复习3
目录 19. 下列关于 do…while 语句和 while 语句的叙述中错误的是( C ) 20. 若有定义 int a9, b6; System.out.println(a > b) 的结果是( D ) 21. 关于接口和抽象类,下列说法正确的是(A) …...

Oracle共享内存不释放
Oracle数据库使用共享内存来管理其系统全局区(SGA)和程序全局区(PGA)。当Oracle数据库的共享内存没有正确释放时,可能会导致数据库启动失败或性能问题。以下是一些可能的原因和解决方法: /dev/shm空间不足&…...