数据结构----图
1.思维导图
2.重要概念的笔记
2.1图的定义与基本术语
2.1.1图的定义
是由一个顶点集 V 和一个顶点间的关系集合组成的数据结构。
结点之间的关系是任意的,图中任意两个数据元素都可能相关。
Graph = (V , VR) 其中,V = { x | x 属于某个数据对象},是顶点的有穷非空集合;VR = {(x, y) | x, y 属于V }是顶点之间关系的有穷集合,也叫做边(edge)或弧(Arc)集合。Path (x, y)表示从 x 到 y 的一条单向通路, 它是有方向的。
2.1.2基本术语
-
带权图:即边上带权的图。其中权是指每条边可以标上具有某种含义的数值(即与边相关的数)
-
有向网:弧或边带权的图且带有方向
-
无向图:弧或边带权的图但不带有方向
-
子图:设有两个图 G=(V, E) 和 G’=(V’,E’)。若 V’⊆V 且 E’⊆E, 则称图G’是图G 的子图。
-
完全图:图G任意两个顶点都有一条边相连接;若 n 个顶点的无向图有n(n-1)/2 条边,称为无向完全图.若n个顶点的有向图有n(n-1)条边, 称为有向完全图。
-
稀疏图:若边或弧的个数 e<nlogn,则称作稀疏图,否则称作稠密图。
-
度:顶点v的度是与它相关联的边的条数。记作D(v)。在有向图中, 顶点的度等于该顶点的入度与出度之和。顶点v
的入度是以v为终点的有向边的条数, 记作ID(v);顶点v 的出度是以v 为始点的有向边的条数, 记作OD(v)。 -
路径: 设图G=(V,{VR})中的一个顶点序列{ u=vi,0,vi,1, …,
vi,m=w}中,(vi,j-1,vi,j)属于VR,1≤j≤m,则称从顶点u 到顶点w 之间存在一条路径。 -
路径长度:路径上边的数目称作路径长度。 例如:从A到F且长度为3的路径为: {A,B,C,F}
-
简单路径:序列中顶点不重复出现的路径。
-
简单回路:序列中第一个顶点和最后一个顶点相同的路径。
-
连通图:若图G中任意两个顶点之间都有路径相通,则称此图为连通图; 若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量。
-
强连通图:对有向图来说若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。 否则,其各个强连通子图称作它的强连通分量。
-
生成树:是一个极小连通子图,它含有图中全部顶点,但只有n-1条边。如果在生成树上添加1条边,必定构成一个环。若图中有n个顶点,却少于n-1条边,必为非连通图。
-
生成森林:由若干棵生成树组成,含全部顶点,但构成这些树的边是最少的。
2.2图的存储结构和基本运算算法
2.2.1邻接矩阵
定义:邻接矩阵,一个存储着边的信息的矩阵,而顶点则用矩阵的下标表示。对于一个邻接矩阵M,如果M(i,j)=1,则说明顶点i和顶点j之间存在一条边,对于无向图来说,M (j ,i) = M (i, j),所以其邻接矩阵是一个对称矩阵;对于有向图来说,则未必是一个对称矩阵,邻接矩阵的对角线元素都为0。
设图 A = (V, E)是一个有 n 个顶点的图,则图的邻接矩阵是一个二维数组 A.edge[n][n]
网络的邻接矩阵
优点:
(1)很容易判断任意两顶点是否有边无边;
(2)要计算某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]=1的vj就是邻接点,而有向图有入度和出度之分:顶点vi的入度为是第i列各数之和,顶点vi的出度是第i行的各数之和。
缺点:
对于边数相对顶点较少的图,浪费了极大的存储空间。
代码实现
typedef int ArcCell;
typedef char VexType;
typedef struct {
VexType vexs[MAXVEXNUM];//点的集合
ArcCell arcs[MAXVEXNUM][MAXVEXNUM];//边的集合
int vexNum, arcNum; //图的当前顶点数和弧数
}MyGraph;
//若图G中存在顶点v,则返回v在图中的位置信息,否则返回其他信息
int LocateVex(MyGraph &G, VexType value)
{
for(int i=0;i<G.vexNum;i++)
{
if(value ==G.vexs[i])
return i;
}
return -1;//图中没有该顶点
}
//采用邻接矩阵表示法构造有向网G
void createMyGraph(MyGraph &G,int vexNum, int arcNum)
{
G.vexNum=vexNum;
G.arcNum=arcNum;
//初始化邻接矩阵
for(int i=0;i<vexNum;i++)
{
for(int j=0;j<arcNum;j++)
G.arcs[i][j]=0;
}
for(int i=0;i<vexNum;i++)
cin>>G.vexs[i];
for(int j=0;j<arcNum;j++)
{
char x,y;
int value;
cin>>x>>y;
cin>>value;
G.arcs[ LocateVex(G,x)][ LocateVex(G,y)]=value;
}
}
//打印邻接矩阵
void DispMyGraph(MyGraph &G)
{
for(int i=0;i<G.vexNum;i++)
{
cout<<G.vexs[i];
for(int j=0;j<G.vexNum;j++)
{
cout<<" "<<G.arcs[i][j];
}
cout<<endl;
}
}
2.2.2邻接表
定义:
- 对于顶点数组,每个数据元素需要存储指向第一个邻接点的指针。
- 每个顶点Vi的所有邻接点构成一个线性表,并用单链表存储;无向图称为顶点Vi的边表,有向图称为顶点Vi为弧尾的出边表
从图中可以看出, 顶点表的各个结点由data和firstedge两个域表示:data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成:adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。
优点:
解决了邻接矩阵空间浪费的问题,容易进行删除和添加
缺点:
对于有向图,出度入度是不兼得的,要两样都获得就只能分别建立、遍历对应的邻接表和逆邻接表。
代码实现:
//图的结构定义:
#define MAXV 20
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef int Vertex;
typedef struct Vnode
{ Vertex data; //顶点信息
ArcNode *firstarc; //指向第一条边
} VNode; //邻接表头节点类型
typedef VNode AdjList[MAXV];
typedef struct
{ AdjList adjlist; //邻接表
int n,e; //图中顶点数n和边数e
} AdjGraph;
//邻接表的创建
void CreateAdj(AdjGraph*& G, int n, int e)
{
int i;
G = new AdjGraph;
G->e = e;
G->n = n;
for (i = 1;i <= n;i++) {
G->adjlist[i].firstarc = NULL; //初始化
}
for (i = 1;i <= e;i++)
{
int a, b;
cin >> a >> b;
ArcNode* p, * q;
p = new ArcNode;
q = new ArcNode;
p->adjvex = b;
q->adjvex = a;
//采用“头插法”在各个顶点的边链头部插入边结点
p->nextarc = G->adjlist[a].firstarc;
G->adjlist[a].firstarc= p;
q->nextarc = G->adjlist[b].firstarc;
G->adjlist[b].firstarc= q;
}
}
2.2.3其他存储方法
十字链表
-
定义顶点表结点结构为:
firstin、firstout 分别指向入边表、出边表中的第一个结点 -
定义边表结点结构为:
tailvex、headvex分别指弧起点(即弧尾)、弧终点(即弧头)在顶点表中的下标。
headlink是指入边表指针域,指向终点相同的下一条边
taillink是指出边表指针域,指向起点相同的下一条边
邻接多重表
定义边表结点结构为:
其中ivex、jvex是与某条边依附的两个顶点在顶点表中的下标,
ilink指向依附顶点 ivex的下一条边,同理,
jlink指向依附顶点 jvex的下一条边。
邻接多重表和邻接表的差别,仅仅在于同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。
2.2.4比较
存储方式 | 优点 | 缺点 | 关注点 |
---|---|---|---|
邻接矩阵 | 便于计算某个顶点的度 | 需求空间大 | 无 |
邻接表 | 节省空间 | 对有向图无法兼备出度入度 | 顶点 |
十字链表 | 对有向图兼得出度入度 | 无 | 有向图、顶点 |
邻接多重表 | 无向图边操作简单 | 无 | 有向图、边 |
2.2.5基本运算算法(以邻接表为例)
#define MAXV 5 //最大顶点个数
#define INF 32767 //定义 ∞
//邻接矩阵
typedef struct vertex {
int number; //顶点的编号
}VertexType; //别名,顶点的类型
typedef struct matrix {
int n; //顶点个数
int e; //边数
int adjMat[MAXV][MAXV]; //邻接矩阵数组
VertexType ver[MAXV]; //存放顶点信息
}MatGraph;
//邻接表
typedef struct eNode {
int adjVer; //该边的邻接点编号
int weight; //该边的的信息,如权值
struct eNode* nextEdge; //指向下一条边的指针
}EdgeNode; //别名,边结点的类型
typedef struct vNode {
EdgeNode* firstEdge; //指向第一个边结点
}VNode; //别名,邻接表的头结点类型
typedef struct list {
int n; //顶点个数
int e; //边数
VNode adjList[MAXV]; //邻接表的头结点数组
}ListGraph; //别名,完整的图邻接表类型
//创建图的邻接表
void createAdjListGraph(ListGraph*& LG, int A[MAXV][MAXV], int n, int e) {
int i, j;
EdgeNode* p;
LG = (ListGraph*)malloc(sizeof(ListGraph));
for (i = 0; i < n; i++) {
LG->adjList[i].firstEdge = NULL; //给邻接表中所有头结点指针域置初值
}
for (i = 0; i < n; i++) { //检查邻接矩阵中的每个元素
for (j = n - 1; j >= 0; j--) {
if (A[i][j] != 0) { //存在一条边
p = (EdgeNode*)malloc(sizeof(EdgeNode)); //申请一个结点内存
p->adjVer = j; //存放邻接点
p->weight = A[i][j]; //存放权值
p->nextEdge = NULL;
p->nextEdge = LG->adjList[i].firstEdge; //头插法
LG->adjList[i].firstEdge = p;
}
}
}
LG->n = n;
LG->e = e;
}
//输出邻接表
void displayAdjList(ListGraph* LG) {
int i;
EdgeNode* p;
for (i = 0; i < MAXV; i++) {
p = LG->adjList[i].firstEdge;
printf("%d:", i);
while (p != NULL) {
if (p->weight != INF) {
printf("%2d[%d]->", p->adjVer, p->weight);
}
p = p->nextEdge;
}
printf(" NULL\n");
}
}
//邻接表转换为邻接矩阵
void ListToMat(ListGraph* LG, MatGraph &MG) {
int i, j;
EdgeNode* p;
for (i = 0; i < MAXV; i++) { //初始化置 0
for (j = 0; j < MAXV; j++) {
MG.adjMat[i][j] = 0;
}
}
for (i = 0; i < LG->n; i++) { //扫描所有单链表
p = LG->adjList[i].firstEdge; //p 指向第 i 个单链表的头结点
while (p != NULL) { //遍历单链表
MG.adjMat[i][p->adjVer] = p->weight;
p = p->nextEdge;
}
}
MG.n = LG->n;
MG.e = LG->e;
}
void destroyAdjListGraph(ListGraph* LG) {
int i;
EdgeNode* pre, * p;
for (i = 0; i < LG->n; i++) {
pre = LG->adjList[i].firstEdge; //挨个释放内存
if (pre != NULL) {
p = pre->nextEdge;
while (p != NULL) {
free(pre);
pre = p;
p = p->nextEdge;
}
free(pre);
}
}
free(LG);
}
2.3图的遍历
2.3.1深度优先
简介:深度优先搜索(Depth_Fisrst Search)遍历类似于树的先根遍历,是树的先根遍历的推广。假设初始状态是图中所有顶点未曾被访问,则深度优先搜索可从图中某个顶点发v 出发,访问此顶点,然后依次从v 的未被访问的邻接点出发深度优先遍历图,直至图中所有和v 有路径相通的顶点都被访问到;若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
以如下图的无向图为例,进行图的深度优先搜索:
搜索过程:
假设从顶点v1 出发进行搜索,在访问了顶点v1 之后,选择邻接点v2。因为v2 未曾访问,则从v2 出发进行搜索。依次类推,接着从v4 、v8 、v5 出发进行搜索。在访问了v5 之后,由于v5 的邻接点都已被访问,则搜索回到v8。由于同样的理由,搜索继续回到v4,v2 直至v1,此时由于v1 的另一个邻接点未被访问,则搜索又从v1 到v3,再继续进行下去由此,得到的顶点访问序列为:
代码实现(以邻接表为例):
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef int Vertex;
typedef struct Vnode
{ Vertex data; //顶点信息
ArcNode *firstarc; //指向第一条边
} VNode; //邻接表头节点类型
typedef VNode AdjList[MAXV];
typedef struct
{ AdjList adjlist; //邻接表
int n,e; //图中顶点数n和边数e
} AdjGraph;
void DFS(AdjGraph* G, int v)
{
static int n = 0; //定义静态变量n,以辨别是否为第一个元素
ArcNode* p;
visited[v] = 1;
if (!n)
{
cout << v;
n++;
}
else
{
cout << " " << v;
n++;
}
p = G->adjlist[v].firstarc;
while (p != NULL) //遍历与p相关联的结点
{
if (visited[p->adjvex] == 0)
DFS(G, p->adjvex); //再次调用DFS
p = p->nextarc;
}
}
2.3.2广度优先
简介:广度优先搜索(Breadth_First Search) 遍历类似于树的按层次遍历的过程。假设从图中某顶点v 出发,在访问了v 之后依次访问v 的各个未曾访问过和邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。换句话说,广度优先搜索遍历图的过程中以v 为起始点,由近至远,依次访问和v 有路径相通且路径长度为1,2,…的顶点。
以如下图的无向图为例,进行广度优先遍历:
搜索过程:
首先访问v1 和v1 的邻接点v2 和v3,然后依次访问v2 的邻接点v4 和v5 及v3 的邻接点v6 和v7,最后访问v4 的邻接点v8。由于这些顶点的邻接点均已被访问,并且图中所有顶点都被访问,由些完成了图的遍历。得到的顶点访问序列为:
代码实现(以邻接表为例):
typedef struct ANode
{ int adjvex; //该边的终点编号
struct ANode *nextarc; //指向下一条边的指针
int info; //该边的相关信息,如权重
} ArcNode; //边表节点类型
typedef int Vertex;
typedef struct Vnode
{ Vertex data; //顶点信息
ArcNode *firstarc; //指向第一条边
} VNode; //邻接表头节点类型
typedef VNode AdjList[MAXV];
typedef struct
{ AdjList adjlist; //邻接表
int n,e; //图中顶点数n和边数e
} AdjGraph;
int visited[MAXV];
void BFS(AdjGraph* G, int v)
{
int w, i;ArcNode* p;
queue<int>q; //创建队列存放结点
cout << v;
visited[v] = 1; //对访问过的结点进行标记
q.push(v);
while (!q.empty()) //队列不为空时进行循环
{
w = q.front();
q.pop();
p = G->adjlist[w].firstarc;
while (p != NULL) //遍历与p相关联的结点
{
if (visited[p->adjvex] == 0)
{
cout << " " << p->adjvex;
visited[p->adjvex] = 1;
q.push(p->adjvex);
}
p = p->nextarc;
}
}
}
2.4图的应用
2.4.1最小生成树
什么是最小生成树
现在假设有一个很实际的问题:我们要在n个城市中建立一个通信网络,则连通这n个城市需要布置n-1一条通信线路,这个时候我们需要考虑如何在成本最低的情况下建立这个通信网?
于是我们就可以引入连通图来解决我们遇到的问题,n个城市就是图上的n个顶点,然后,边表示两个城市的通信线路,每条边上的权重就是我们搭建这条线路所需要的成本,所以现在我们有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个通信网,当我们构造这个连通网所花的成本最小时,搭建该连通网的生成树,就称为最小生成树。
最小生成树的MST的性质:
假设N=(V,{E})是一个连通网,U是顶点集V的一个非空子集,若(u,v)是一条具有最小权值(代价)的边,其中u属于U, v属于V-U,则必存在一棵包含边(u,v)的最小生成树。
2.4.2普里姆算法
算法思路:
首先就是从图中的一个起点a开始,把a加入U集合,然后,寻找从与a有关联的边中,权重最小的那条边并且该边的终点b在顶点集合:**(V-U)**中,我们也把b加入到集合U中,并且输出边(a,b)的信息,这样我们的集合U就有:{a,b},然后,我们寻找与a关联和b关联的边中,权重最小的那条边并且该边的终点在集合:(V-U)中,我们把c加入到集合U中,并且输出对应的那条边的信息,这样我们的集合U就有:{a,b,c}这三个元素了,一次类推,直到所有顶点都加入到了集合U。
代码实现:
void prim(MatGraph g,int v)
{
int lowcost[MAXV];
int Min;
int closest[MAXV],i,j,k;
for(i=0;i<g.n;i++) //给lowcost[]和closest[]置初值
{
lowcost[i]=g.edge[v][i];
closest[i]=v;
}
for(i=1;i<g.n;i++)
{
Min=INF;
for(j=0;j<g.n;j++)
if(lowcost[j]!=0&&lowcost[j]<Min)
{
Min=lowcost[j];
k=j; //k点记录最近编号
}
printf("边(%d,%d)权为:%d\n",closest[k],k,Min); //输出最小生成树的一条边
lowcost[k]=0;
for(j=0;j<g.n;j++)
if(lowcost[j]!=0&&g.edge[k][j]<lowcost[j])
{
lowcost[j]=g.edge[k][j];
closest[j]=k;
}
}
}
2.4.3克鲁斯卡尔算法
算法思路:
将图中的所有边都去掉,然后将边按权值从小到大的顺序添加到图中,保证添加的过程中不会形成环,重复上一步直到连接所有顶点,此时就生成了最小生成树。
例如以下所示无向图,采用Kruskal算法构建最小生成树过程如下。
(1)首先将所有的边按照代价大小进行排序,排序结果为(B,D),(B,F)(A,C),(B,C),(A,B),(D,F),(E,F),(C,E)。(2)代价最小边为(B,D),顶点B、D不在同一棵树上,将顶点B、D合并到一棵子树。
(3)代价最小边为(B,F),顶点B、F不在同一棵树上,将顶点B、F合并到一棵子树。(4)代价最小边为(A、C),顶点A、C不在同一棵树上,将顶点A、C合并到一棵子树。
(5)代价最小边为(B,C),顶点B、C不在同一棵树上,将顶点B、C合并到一棵子树。(6)代价最小边为(A,B),顶点A、B在同一棵树上,因此不能选择此边。(7)代价最小边为(D,F),顶点D、F在同一棵树上,因此不能选择此边。(8)代价最小边为(E,F),顶点E、F不在同一棵树上,将顶点E、F合并到一棵子树。(9)代价最小边为(C,E),顶点C、E在同一棵树上,因此不能选择此边。
(10)所有顶点均在同一棵树内,生成过程完毕。最小生成树为:
代码实现:
与Prim算法比较
算法名 | Prim算法 | Kruskal算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n^2)(n为顶点数) | O(eloge)(e为边数) |
适应范围 | 稠密图 | 稀疏图 |
2.4.4最短路径
定义:两个顶点之间带权路径长度最短的路径,在带权图当中,把一个顶点v到另一个顶点u所经历的边的权值之和称为,路径的带权路径长度。
2.4.4.1 Dijkstra算法
基本思路:
通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算)。
此外,引进两个集合S和U。S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)。
初始时,S中只有起点s;U中是除s之外的顶点,并且U中顶点的路径是”起点s到该顶点的路径”。然后,从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。
然后,再从U中找出路径最短的顶点,并将其加入到S中;接着,更新U中的顶点和顶点对应的路径。 … 重复该操作,直到遍历完所有顶点。
以下图为例,对Dijkstra算法进行演示。
此时,起点D到各个顶点的最短距离就计算出来了:A(22) B(13) C(3) D(0) E(4) F(6) G(12)。
代码实现:
/*
* Dijkstra最短路径。
* 即,统计图(G)中"顶点vs"到其它各个顶点的最短路径。
*
* 参数说明:
* G -- 图
* vs -- 起始顶点。即计算"顶点vs"到其它顶点的最短路径。
* prev -- 前驱顶点数组。即,prev[i]的值是"顶点vs"到"顶点i"的最短路径所经历的全部顶点中,位于"顶点i"之前的那个顶点。
* dist -- 长度数组。即,dist[i]是"顶点vs"到"顶点i"的最短路径的长度。
*/
void dijkstra(Graph G, int vs, int prev[], int dist[])
{
int i,j,k;
int min;
int tmp;
int flag[MAX]; // flag[i]=1表示"顶点vs"到"顶点i"的最短路径已成功获取。
// 初始化
for (i = 0; i < G.vexnum; i++)
{
flag[i] = 0; // 顶点i的最短路径还没获取到。
prev[i] = 0; // 顶点i的前驱顶点为0。
dist[i] = G.matrix[vs][i];// 顶点i的最短路径为"顶点vs"到"顶点i"的权。
}
// 对"顶点vs"自身进行初始化
flag[vs] = 1;
dist[vs] = 0;
// 遍历G.vexnum-1次;每次找出一个顶点的最短路径。
for (i = 1; i < G.vexnum; i++)
{
// 寻找当前最小的路径;
// 即,在未获取最短路径的顶点中,找到离vs最近的顶点(k)。
min = INF;
for (j = 0; j < G.vexnum; j++)
{
if (flag[j]==0 && dist[j]<min)
{
min = dist[j];
k = j;
}
}
// 标记"顶点k"为已经获取到最短路径
flag[k] = 1;
// 修正当前最短路径和前驱顶点
// 即,当已经"顶点k的最短路径"之后,更新"未获取最短路径的顶点的最短路径和前驱顶点"。
for (j = 0; j < G.vexnum; j++)
{
tmp = (G.matrix[k][j]==INF ? INF : (min + G.matrix[k][j])); // 防止溢出
if (flag[j] == 0 && (tmp < dist[j]) )
{
dist[j] = tmp;
prev[j] = k;
}
}
}
// 打印dijkstra最短路径的结果
printf("dijkstra(%c): \n", G.vexs[vs]);
for (i = 0; i < G.vexnum; i++)
printf(" shortest(%c, %c)=%d\n", G.vexs[vs], G.vexs[i], dist[i]);
}
2.4.4.2Floyd算法
基本思路
从节点i到节点j的最短路径不外乎两种可能:1)直接从i到j; 2) i经过若干节点再到j;所以我们可以这样来计算i j之间的最短距离:对于每一个结点k,我们判断Dist(i,k)+Dist(k,j)<Dist(i,j)是否成立,如果成立,则证明从i到k,再从k到j的距离比直接从i到j的距离短,所以我们更新Dist(i,j)=Dist(i,k)+Dist(k,j); 这样,遍历完所有的k值,则得到最终从i到j的最小距离。
Floyd算法过程:
1、用D[i][j]记录每两个顶点之间的距离;
2、依次扫描每一个顶点k,以该点为基准,判断从i经过k,再到j的距离是否小于D[i][j],若是则更新D[i][j]=D[i][k]+D[k][j];
注意:依次扫描每一点(k),并以该点作为中介点,计算出通过k点的其他任意两点(i,j)的最短距离,这就是floyd算法的精髓!同时也解释了为什么k点这个中介点要放在算法最外层循环的原因.
代码实现:
typedef struct
{
char vertex[MAXVER];
int edges[MAXVER][MAXVER];
int n, e;
}MGraph;
void Floyd(MGraph g)
{
int D[MAXVER][MAXVER];
int path[MAXVER][MAXVER];
int n = g.n;
for (int i = 0; i < n; ++i) //初始化
{
for (int j = 0; j < n; ++j)
{
D[i][j] = g.edges[i][j];
path[i][j] = -1;
}
}
for (int k = 0; k < n; k++)
{
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
if (D[i][j]>D[i][k] + D[k][j])
{
D[i][j] = D[i][k] + D[k][j];
path[i][j] = k;
}
}
}
}
}
2.4.6拓扑排序
简介:对一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。
AOV网:把顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网,简称AOV网。
一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列,由AOV网构造拓扑序列的过程叫做拓扑排序。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
算法步骤:
- 在有向图中选一个没有前驱的顶点并且输出。
- 从图中删除该顶点和所有以它为尾的弧,即删除所有和它有关的边。
- 重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。
时间复杂度:O(n+e)
代码实现:
typedef char VertexType;
typedef int EdgeType;
//邻接节点结构
typedef struct EdgeNode
{
int adjvex;
EdgeType weight;
struct EdgeNode *next;
}EdgeNode;
//顶点节点列表
typedef struct VertexNode
{
VertexType data;
EdgeNode *firstedge;
}VertexNode,AdjList[MAXVEX];
//图的总述结构
typedef struct
{
AdjList adjList;
int numVertexes,numEdges;
}GraphAdjList;
int *etv,*ltv;
int *stack2;
int top2;
/*
利用拓补排序判断一个网是否有环路
*/
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i,k,gettop;
int top = 0;
int count = 0;
int *stack;
stack = (int *)malloc(GL->numVertexes * sizeof(int));
for(i=0;i<GL->numVertexes;i++)
{
if(0 == GL->adjLIst[i].in)
{
top = top + 1;
stack[top] = i;
}
}
top2 = 0;
ltv = (int *)malloc(GL->numVertexes * sizeof(int));
for(i=0;i<GL->numVertexes;i++)
{
ltv[i] = 0;
}
stack2 = (int *)malloc(GL->numVertexes * sizeof(int));
while(0 != top)
{
gettop = stack[top];
top = top - 1;
count = count + 1;
top2 = top2 + 1;
stack2[top2] = gettop;
for(e=GL->adjList[gettop].firstedge;e;e=e->next)
{
k = e->adjvex;
if(!(--GL->agjList[k].in))
{
top = top + 1;
stack[top] = k;
}
/*
计算事件发生最晚时间,即走最大路径
*/
if((ltv[gettop]+e->weight)>ltv[k])
{
ltv[k] = ltv[gettop] + e->weight;
}
}
}
if(count < GL->numVertexes)
{
return ERROR;
}
else
{
return OK;
}
}
2.4.7关键路径
关键路径:AOE-网中,从起点到终点最长的路径的长度(长度指的是路径上边的权重和)。
关键活动:关键路径上的边。
首先我们假设活动a(i)是弧<j,k>上的活动,j为弧尾顶点,k为弧头(有箭头的一边), ve(j)代表的是弧尾j的最早发生时间,
vl(k)代表的是弧头k的最迟发生时间 dut(<j,k>)代表该活动要持续的时间,既是弧的权值
则: e(i)=ve(j)
l(i)=vl(k)-dut(<j,k>)
求关键路径的步骤
输入顶点数和边数,已经各个弧的信息建立图
从源点v1出发,令ve[0]=0;按照拓扑序列往前求各个顶点的ve。如果得到的拓扑序列个数小于网的顶点数n,说明我们建立的图有环,无关键路径,直接结束程序
从终点vn出发,令vl[n-1]=ve[n-1],按逆拓扑序列,往后求其他顶点vl值
根据各个顶点的ve和vl求每个弧的e(i)和l(i),如果满足e(i)=l(i),说明是关键活动。
代码实现:
void CriticalPath(GraphAdjList GL)
{
EdgeNode *e;
int i,gettop,k,j;
int ete,lte;
TopologicalSort(GL); //拓补排序
etv = (int *)malloc(GL->numVertexes * sizeof(int));
/*
用终点(汇点)的最晚发生时间来初始化其他节点的最早发生时间
对终点来说,最晚发生时间=最早发生时间
*/
for(i=0;i<GL->numVertexes;i++)
{
etv[i] = ltv[GL->numVertexes - 1];
}
/*
按拓补序列逆序计算事件的最早发生时间
*/
while(0 != top2)
{
gettop = stack2[top2--];
for(e=GL->adjList[gettop].firstedge;e;e=e->next)
{
/*
k此时是与gettop邻接的边的终点
*/
k = e->adjvex;
/*
e->weight是从gettop到k这个边的权值
最早发生时间=k节点的最早发生时间-耗时最长的权重
*/
if(etv[k] - e->weight < etv[gettop])
{
etv[gettop] = etv[k] - e->weight;
}
}
}
/*
ete:活动的最早开工时间
lte:活动的最晚开工时间
etv:事件的最早发生时间
ltv:事件的最晚发生时间
*/
for(j=0;j<GL->numVertexes;j++)
{
for(e=GL->adjList[j].firstedge;e;e=e->next)
{
// k是边的终点,j是边的起点,e是指j到k这条边
k = e->adjvex;
lte = ltv[j];
/*
活动的最早发生时间=活动终点事件最早发生时间-活动耗时(权重)
*/
ete = etv[k] - e->weight;
/*
比较的是活动的最早和最晚时间
*/
if(ete == lte)
{
printf("<v%d,v%d> length:%d ",GL->adjList[j].data,GL->adjList[k].data,e->weight);
}
}
}
}
3.疑难问题及解决方案
解决思路:
本题使用Prim算法思想,找到最小值,但这题用图存数据然后运用prim算法会很容易超时,所以通过输入数据,找到合适的化简方法,通过输入的路径权重升序排序,然后每次遍历这组数据,找到最短的一条路径满足一个点已经被读取,另一个点没被读取,然后这条路径就是要找的,然后把未读取的点标记为已读取,并sum+=该路径权重,然后重新循环,若中途有一次遍历完这组数据都找不到该路径,则该图应该不是连通图,返回-1,结束函数。
代码实现:
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
typedef struct Node {
int S;
int E;
int value;
}Road;
bool BJ(Road m,Road n){//比较函数
if (m.value < n.value)
return true;
return false;
}
Road X[3001];
int N, M;
void HVV();
int Lowcost();
int main()
{
cin >> N >> M;
HVV();
cout << Lowcost() << flush;
return 0;
}
void HVV()
{
int i, a, b, c;
for (i = 0; i < M; i++) {
cin >> a >> b >> c;
X[i].E = a;
X[i].S = b;
X[i].value = c;
}
sort(X,X+M,BJ);//升序排序
}
int Lowcost()
{
int x[1001] = { 0 };
int sum = 0;//记录最低费用
x[X[0].E] = 1;//把最小的路径的两个点都标记并且sum+=该路径权值
x[X[0].S] = 1;
sum += X[0].value;
int i, j;
for (i = 2; i < N; i++) {
for (j = 1; j < M; j++) {
if (x[X[j].E] && !x[X[j].S]) {//一个点标记一个点没标记
sum += X[j].value;
x[X[j].S] = 1;
break;
}
else if (!x[X[j].E] && x[X[j].S]) {//一个点标记一个点没标记
sum += X[j].value;
x[X[j].E] = 1;
break;
}
}
if (j == M)
return -1;
}
return sum;
}