数据结构----图

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网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
算法步骤

  1. 在有向图中选一个没有前驱的顶点并且输出。
  2. 从图中删除该顶点和所有以它为尾的弧,即删除所有和它有关的边。
  3. 重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。
    时间复杂度: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;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值